nodepool/nodepool/tests/unit/fake_ibmvpc.py

716 lines
25 KiB
Python

# Copyright (C) 2021 Acme Gating, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import time
import re
import urllib
import uuid
import fixtures
import responses
class CRUDManager:
name = ''
list_name = None
def __init__(self, cloud):
self.cloud = cloud
self.items = []
self.requests = []
def list(self, request):
listname = self.list_name or self.name
resp = {
"limit": 50,
"first": {
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
f"{self.name}?limit=50")
},
"total_count": len(self.items),
listname: self.items,
}
return (200, {}, json.dumps(resp))
def get(self, request):
url = urllib.parse.urlparse(request.path_url)
uid = url.path.split('/')[-1]
item = self.lookup(id=uid)
if item:
return (200, {}, json.dumps(item))
else:
return (404, {}, json.dumps({
'message': 'Item not found',
}))
def lookup(self, **kw):
for item in self.items:
for k, v in kw.items():
if item[k] == v:
return item
class ResourceGroupsCRUD(CRUDManager):
name = "resources"
def make(self, name):
uid = str(uuid.uuid4().hex)
uid2 = str(uuid.uuid4().hex)
data = {
"id": uid,
"crn": ("crn:v1:bluemix:public:resource-controller::a/"
f"{self.cloud.account_id}::resource-group:{uid}"),
"account_id": self.cloud.account_id,
"name": name,
"state": "ACTIVE",
"default": False,
"enable_reclamation": False,
"quota_id": uid2,
"quota_url": f"/v2/quota_definitions/{uid2}",
"payment_methods_url": (f"/v2/resource_groups/{uid}"
"/payment_methods"),
"resource_linkages": [],
"teams_url": f"/v2/resource_groups/{uid}/teams",
"created_at": "2021-12-01T20:41:25.202Z",
"updated_at": "2021-12-01T20:41:25.202Z"
}
return data
def post(self, request):
data = json.loads(request.body)
obj = self.make(data['name'])
self.items.append(obj)
return (200, {}, json.dumps(obj))
class ResourceInstancesCRUD(CRUDManager):
name = "resource_instances"
list_name = "resources"
def make(self, name, kind, resource_group):
uid = str(uuid.uuid4().hex)
uid2 = str(uuid.uuid4().hex)
uid3 = str(uuid.uuid4().hex)
data = {
"id": (f"crn:v1:bluemix:public:{kind}:global:"
f"a/{self.cloud.account_id}:{uid}::"),
"guid": uid,
"url": f"/v2/resource_instances/{uid}",
"created_at": "2021-12-11T01:01:19.588831392Z",
"updated_at": "2021-12-11T01:01:21.814042061Z",
"deleted_at": None,
"created_by": "IBMid-something",
"updated_by": "",
"deleted_by": "",
"scheduled_reclaim_at": None,
"restored_at": None,
"scheduled_reclaim_by": "",
"restored_by": "",
"name": name,
"region_id": "global",
"account_id": f"{self.cloud.account_id}",
"reseller_channel_id": "",
"resource_plan_id": uid2,
"resource_group_id": resource_group['id'],
"resource_group_crn": resource_group['crn'],
"target_crn": (f"crn:v1:bluemix:public:globalcatalog::::"
f"deployment:{uid2}%3Aglobal"),
"allow_cleanup": False,
"crn": (f"crn:v1:bluemix:public:{kind}:global:"
f"a/{self.cloud.account_id}:{uid}::"),
"state": "active",
"type": "service_instance",
"resource_id": uid3,
"dashboard_url": "https://cloud.ibm.com/something/",
"last_operation": {
"type": "create",
"state": "succeeded",
"async": False,
"description": "Completed create instance operation"
},
"resource_aliases_url": (
f"/v2/resource_instances/{uid}/resource_aliases"),
"resource_bindings_url": (
f"/v2/resource_instances/{uid}/resource_bindings"),
"resource_keys_url": f"/v2/resource_instances/{uid}/resource_keys",
"plan_history": [
{
"resource_plan_id": uid2,
"start_date": "2021-12-11T01:01:19.588831392Z",
"requestor_id": "IBMid-something"
}
],
"migrated": False,
"controlled_by": "",
"locked": False,
}
return data
class ProfilesCRUD(CRUDManager):
name = "profiles"
def make(self, name, cpu, ram):
data = {
'bandwidth': {'type': 'fixed', 'value': 4000},
'disks': [],
'family': 'compute',
'href': ('https://us-south.iaas.cloud.ibm.com/v1/instance/'
f'profiles/{name}'),
'memory': {'type': 'fixed', 'value': ram},
'name': 'cx2-2x4',
'os_architecture': {
'default': 'amd64', 'type': 'enum', 'values': ['amd64']
},
'port_speed': {'type': 'fixed', 'value': 16000},
'total_volume_bandwidth': {
'default': 1000, 'max': 3500, 'min': 500,
'step': 1, 'type': 'range'
},
'vcpu_architecture': {'type': 'fixed', 'value': 'amd64'},
'vcpu_count': {'type': 'fixed', 'value': cpu}
}
return data
class VpcsCRUD(CRUDManager):
name = "vpcs"
def make(self, name, resource_group):
uid = str(uuid.uuid4())
net_acl = str(uuid.uuid4())
routing_table = str(uuid.uuid4())
security_group = str(uuid.uuid4())
data = {
"id": uid,
"crn": ("crn:v1:bluemix:public:is:us-south:"
f"a/{self.cloud.account_id}::vpc:{uid}"),
"href": f"https://us-south.iaas.cloud.ibm.com/v1/vpcs/{uid}",
"name": name,
"resource_type": "vpc",
"status": "available",
"classic_access": False,
"created_at": "2021-12-09T17:11:13Z",
"default_network_acl": {
"id": net_acl,
"crn": ("crn:v1:bluemix:public:is:us-south:"
f"a/{self.cloud.account_id}::network-acl:{net_acl}"),
"href": ("https://us-south.iaas.cloud.ibm.com/v1/network_acls/"
f"{net_acl}"),
"name": net_acl,
},
"default_routing_table": {
"id": routing_table,
"href": ("https://us-south.iaas.cloud.ibm.com/v1/vpcs/"
f"{uid}/routing_tables/{routing_table}"),
"name": routing_table,
"resource_type": "routing_table"
},
"default_security_group": {
"id": security_group,
"crn": ("crn:v1:bluemix:public:is:us-south:"
f"a/{self.cloud.account_id}::"
f"security-group:{security_group}"),
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
f"security_groups/{security_group}"),
"name": security_group,
},
"resource_group": {
"id": resource_group['id'],
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
f"resource_groups/{resource_group['id']}"),
"name": resource_group['name']
},
"cse_source_ips": [
{
"ip": {"address": "10.16.234.155"},
"zone": {
"name": "us-south-1",
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"regions/us-south/zones/us-south-1")
}
},
{
"ip": {"address": "10.16.246.112"},
"zone": {
"name": "us-south-2",
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"regions/us-south/zones/us-south-2")
}
},
{
"ip": {"address": "10.16.250.50"},
"zone": {
"name": "us-south-3",
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"regions/us-south/zones/us-south-3")
}
}
]
}
return data
class SubnetsCRUD(CRUDManager):
name = "subnets"
def make(self, name, vpc):
uid = str(uuid.uuid4())
data = {
"id": uid,
"crn": ("crn:v1:bluemix:public:is:us-south-1:"
f"a/{self.cloud.account_id}::subnet:{uid}"),
"href": f"https://us-south.iaas.cloud.ibm.com/v1/subnets/{uid}",
"name": name,
"resource_type": "subnet",
"available_ipv4_address_count": 250,
"ipv4_cidr_block": "10.240.0.0/24",
"ip_version": "ipv4",
"zone": {
"name": "us-south-1",
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"regions/us-south/zones/us-south-1")
},
"vpc": {
"id": vpc['id'],
"crn": ("crn:v1:bluemix:public:is:us-south:"
f"a/{self.cloud.account_id}::vpc:{vpc['id']}"),
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
f"vpcs/{vpc['id']}"),
"name": vpc['name'],
"resource_type": "vpc"
},
"status": "available",
"total_ipv4_address_count": 256,
"created_at": "2021-12-09T17:11:18Z",
"network_acl": vpc['default_network_acl'],
"resource_group": vpc['resource_group'],
"routing_table": vpc['default_routing_table'],
}
return data
class KeysCRUD(CRUDManager):
name = "keys"
def make(self, name, vpc):
uid = str(uuid.uuid4())
data = {
"created_at": "2021-12-09T17:14:53Z",
"crn": ("crn:v1:bluemix:public:is:us-south:"
f"a/{self.cloud.account_id}::key:{uid}"),
"fingerprint": "SHA256:something",
"href": f"https://us-south.iaas.cloud.ibm.com/v1/keys/{uid}",
"id": uid,
"length": 2048,
"name": name,
"public_key": "ssh-rsa something\n",
"resource_group": vpc['resource_group'],
"type": "rsa"
}
return data
class FloatingIPsCRUD(CRUDManager):
name = "floating_ips"
def addTarget(self, fip, instance):
if 'target' in fip:
raise Exception("FIP already has target")
fip['target'] = {
"href": instance['primary_network_interface']['href'],
"id": instance['primary_network_interface']['id'],
"name": instance['primary_network_interface']['name'],
"primary_ipv4_address": instance['primary_network_interface'
]['primary_ipv4_address'],
"resource_type": "network_interface"
}
def removeTarget(self, fip):
del fip['target']
def make(self, name, resource_group):
uid = str(uuid.uuid4())
addr = 'fake'
data = {
"address": addr,
"created_at": "2021-12-11T00:07:08Z",
"crn": ("crn:v1:bluemix:public:is:us-south-1:"
f"a/{self.cloud.account_id}::floating-ip:{uid}"),
"href": ("https://us-south.iaas.cloud.ibm.com/"
f"v1/floating_ips/{uid}"),
"id": uid,
"name": name,
"resource_group": {
"id": resource_group['id'],
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
f"resource_groups/{resource_group['id']}"),
"name": resource_group['name']
},
"status": "available",
"zone": {
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"regions/us-south/zones/us-south-1"),
"name": "us-south-1"
}
}
return data
def post(self, request):
data = json.loads(request.body)
resource_group = self.cloud.crud['resources'].lookup(
id=data['resource_group']['id'])
zone = data['zone']['name']
# Make sure this argument is supplied
if zone != 'us-south-1':
raise Exception("Unsupported zone")
obj = self.make(data['name'], resource_group)
self.items.append(obj)
return (200, {}, json.dumps(obj))
def delete(self, request):
url = urllib.parse.urlparse(request.path_url)
uid = url.path.split('/')[-1]
instance = self.lookup(id=uid)
if instance:
self.items.remove(instance)
else:
return (404, {}, json.dumps({
'message': 'Floating IP not found',
}))
return self.list(request)
class InstancesCRUD(CRUDManager):
name = "instances"
def make(self, name, vpc, profile, image):
uid = str(uuid.uuid4())
boot_volume_id = str(uuid.uuid4())
boot_volume_attachment_id = str(uuid.uuid4())
nic_id = str(uuid.uuid4())
subnet = self.cloud.crud['subnets'].lookup(name='sn-nodepool')
nic = {
"href": ("https://us-south.iaas.cloud.ibm.com/v1/instances/"
f"{uid}/network_interfaces/{nic_id}"),
"id": nic_id,
"name": "eth0",
"primary_ipv4_address": f"10.240.0.{len(self.items) + 1}",
"resource_type": "network_interface",
"subnet": {
"crn": subnet['crn'],
"href": subnet['href'],
"id": subnet['id'],
"name": subnet['name'],
"resource_type": "subnet"
}
}
boot_volume_attachment = {
"device": {"id": f"{boot_volume_attachment_id}-25fcc"},
"href": ("https://us-south.iaas.cloud.ibm.com/v1/instances/"
f"{uid}/volume_attachments/{boot_volume_attachment_id}"),
"id": boot_volume_attachment_id,
"name": boot_volume_attachment_id,
"volume": {
"crn": ("crn:v1:bluemix:public:is:us-south-1:"
f"a/{self.cloud.account_id}::volume:{boot_volume_id}"),
"href": ("https://us-south.iaas.cloud.ibm.com/v1/volumes/"
f"{boot_volume_id}"),
"id": boot_volume_id,
"name": f"{name}-boot-1639181087000"
}
}
data = {
"bandwidth": 4000,
"boot_volume_attachment": boot_volume_attachment,
"created_at": "2021-12-11T00:05:16Z",
"crn": ("crn:v1:bluemix:public:is:us-south-1:"
f"a/{self.cloud.account_id}::instance:{uid}"),
"disks": [],
"href": f"https://us-south.iaas.cloud.ibm.com/v1/instances/{uid}",
"id": uid,
"image": {
"crn": image['crn'],
"href": image['href'],
"id": image['id'],
"name": image['name'],
},
"memory": 4,
"name": name,
"network_interfaces": [nic],
"primary_network_interface": nic,
"profile": {
"href": profile['href'],
"name": profile['name'],
},
"resource_group": vpc['resource_group'],
"resource_type": "instance",
"startable": True,
"status": "pending",
"status_reasons": [],
"total_network_bandwidth": 3000,
"total_volume_bandwidth": 1000,
"vcpu": {
"architecture": "amd64",
"count": 2
},
"volume_attachments": [boot_volume_attachment],
"vpc": {
"crn": vpc['crn'],
"href": vpc['href'],
"id": vpc['id'],
"name": vpc['name'],
"resource_type": "vpc"
},
"zone": {
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"regions/us-south/zones/us-south-1"),
"name": "us-south-1"
}
}
return data
def post(self, request):
data = json.loads(request.body)
vpc = self.cloud.crud['vpcs'].lookup(id=data['vpc']['id'])
profile = self.cloud.crud['profiles'].lookup(
name=data['profile']['name'])
image = self.cloud.crud['images'].lookup(id=data['image']['id'])
obj = self.make(data['name'], vpc, profile, image)
self.items.append(obj)
ret = json.dumps(obj)
# Finish provisioning after return
obj['status'] = 'running'
return (200, {}, ret)
def delete(self, request):
url = urllib.parse.urlparse(request.path_url)
uid = url.path.split('/')[-1]
instance = self.lookup(id=uid)
if instance:
self.items.remove(instance)
else:
return (404, {}, json.dumps({
'message': 'Instance not found',
}))
return self.list(request)
class ImagesCRUD(CRUDManager):
name = "images"
def make(self, name, operating_system, owner_type, resource_group=None):
uid = str(uuid.uuid4())
if resource_group:
rg = {
"id": resource_group['id'],
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
f"resource_groups/{resource_group['id']}"),
}
else:
rgid = str(uuid.uuid4())
rg = {
"id": rgid,
"href": ("https://resource-controller.cloud.ibm.com/"
f"v1/resource_groups/{rgid}")
}
data = {
"id": uid,
"crn": ("crn:v1:bluemix:public:is:us-south:"
f"a/something::image:{uid}"),
"href": f"https://us-south.iaas.cloud.ibm.com/v1/images/{uid}",
"name": name,
"minimum_provisioned_size": 100,
"resource_group": rg,
"created_at": "2020-01-11T01:01:01Z",
"file": {
"checksums": {"sha256": "something"},
"size": 2
},
"operating_system": operating_system,
"status": "available",
"visibility": "public",
"encryption": "none",
"status_reasons": [],
"owner_type": owner_type,
}
return data
def post(self, request):
data = json.loads(request.body)
resource_group = self.cloud.crud['resources'].lookup(
id=data['resource_group']['id'])
os = self.cloud.operating_systems[data['operating_system']['name']]
obj = self.make(data['name'], os, 'user', resource_group)
self.items.append(obj)
return (200, {}, json.dumps(obj))
def delete(self, request):
url = urllib.parse.urlparse(request.path_url)
uid = url.path.split('/')[-1]
instance = self.lookup(id=uid)
if instance:
self.items.remove(instance)
else:
return (404, {}, json.dumps({
'message': 'Image not found',
}))
return self.list(request)
class FakeIBMVPCFixture(fixtures.Fixture):
operating_systems = {
'debian-9-amd64': {
"href": ("https://us-south.iaas.cloud.ibm.com/v1/"
"operating_systems/debian-9-amd64"),
"name": "debian-9-amd64",
"architecture": "amd64",
"display_name": ("Debian GNU/Linux 9.x Stretch/Stable - "
"Minimal Install (amd64)"),
"family": "Debian GNU/Linux",
"vendor": "Debian",
"version": "9.x Stretch/Stable - Minimal Install",
"dedicated_host_only": False
},
}
def _setUp(self):
self.crud = {}
self.responses = responses.RequestsMock()
self.responses.start()
self.account_id = str(uuid.uuid4())
self._setup_crud(
'https://resource-controller.cloud.ibm.com/v2/resource_groups',
ResourceGroupsCRUD)
p = self.crud['resources']
p.items.append(p.make('Default'))
self._setup_crud(
r'https://resource-controller.cloud.ibm.com/v2/'
r'resource_instances\?name=(.*?)&type=(.*?)',
ResourceInstancesCRUD)
p = self.crud['resource_instances']
p.items.append(p.make(
"Cloud Object Storage-r6", "cloud-object-storage",
self.crud['resources'].lookup(name='Default')))
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/vpcs',
VpcsCRUD)
p = self.crud['vpcs']
p.items.append(p.make(
'nodepool', self.crud['resources'].lookup(name='Default')))
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/subnets',
SubnetsCRUD)
p = self.crud['subnets']
p.items.append(p.make(
'sn-nodepool', self.crud['vpcs'].lookup(name='nodepool')))
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/keys',
KeysCRUD)
p = self.crud['keys']
p.items.append(p.make(
'testuser', self.crud['vpcs'].lookup(name='nodepool')))
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/instance/profiles',
ProfilesCRUD)
p = self.crud['profiles']
p.items.append(p.make('cx2-2x4', 2, 4))
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/floating_ips',
FloatingIPsCRUD)
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/instances',
InstancesCRUD)
self._setup_crud(
'https://us-south.iaas.cloud.ibm.com/v1/images',
ImagesCRUD)
p = self.crud['images']
p.items.append(p.make('ibm-debian-9-13-minimal-amd64-4',
self.operating_systems['debian-9-amd64'],
'provider'))
self.attach_fip_re = re.compile(
r'https://us-south.iaas.cloud.ibm.com/v1/instances/(.*?)'
r'/network_interfaces/(.*?)'
r'/floating_ips/(.*?)'
r'\?version=(.+?)&generation=(\d+)')
self.responses.add_callback(
responses.PUT,
self.attach_fip_re,
callback=self._attach_floating_ip,
content_type='application/json')
self.responses.add(
responses.POST,
'https://iam.cloud.ibm.com/identity/token',
json={
'token_type': 'Bearer',
'access_token': 'secret_token',
'expires_in': 600,
'expiration': time.time() + 600,
})
self.addCleanup(self.responses.stop)
self.addCleanup(self.responses.reset)
def _setup_crud(self, url, manager):
self.crud[manager.name] = manager(self)
list_re = re.compile(
'^' + url + r'(\?version=(.+?)&generation=(\d+))?$')
crud_re = re.compile(
'^' + url + r'/(.+?)(\?version=(.+?)&generation=(\d+))?$')
self.responses.add_callback(
responses.GET, list_re, callback=self.crud[manager.name].list,
content_type='application/json')
self.responses.add_callback(
responses.GET, crud_re, callback=self.crud[manager.name].get,
content_type='application/json')
if hasattr(self.crud[manager.name], 'post'):
self.responses.add_callback(
responses.POST, list_re, callback=self.crud[manager.name].post,
content_type='application/json')
if hasattr(self.crud[manager.name], 'delete'):
self.responses.add_callback(
responses.DELETE, crud_re,
callback=self.crud[manager.name].delete,
content_type='application/json')
def _attach_floating_ip(self, request):
m = self.attach_fip_re.match(request.url)
instance_id, nic_id, fip_id, version, generation = m.groups()
fip = self.crud['floating_ips'].lookup(id=fip_id)
instance = self.crud['instances'].lookup(id=instance_id)
if nic_id != instance['primary_network_interface']['id']:
raise Exception("Unsupported nic id")
self.crud['floating_ips'].addTarget(fip, instance)
return (200, {}, json.dumps(fip))