зеркало из https://github.com/Azure/ipam.git
Added missing APIs to update external networks, subnets and endpoints, updated NPM packages and updated tests to cover new APIs
This commit is contained in:
Родитель
35bb35aa21
Коммит
87a55ef74b
|
@ -152,7 +152,10 @@ async def ipam_init():
|
|||
"env": globals.AZURE_ENV
|
||||
}
|
||||
|
||||
requests.post(url = "https://azureipammetrics.azurewebsites.net/api/heartbeat", json = hb_message)
|
||||
try:
|
||||
requests.post(url = "https://azureipammetrics.azurewebsites.net/api/heartbeat", json = hb_message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def upgrade_db():
|
||||
managed_identity_credential = ManagedIdentityCredential(
|
||||
|
|
|
@ -445,7 +445,7 @@ class ExtSubnetReq(BaseModel):
|
|||
|
||||
return data
|
||||
|
||||
class ExtEndpointUpdate(BaseModel):
|
||||
class ExtEndpointReq(BaseModel):
|
||||
"""DOCSTRING"""
|
||||
|
||||
name: str
|
||||
|
@ -463,6 +463,12 @@ SpaceUpdate = Annotated[List[JSONPatch], None]
|
|||
|
||||
BlockUpdate = Annotated[List[JSONPatch], None]
|
||||
|
||||
ExtNetUpdate = Annotated[List[JSONPatch], None]
|
||||
|
||||
ExtSubnetUpdate = Annotated[List[JSONPatch], None]
|
||||
|
||||
ExtEndpointUpdate = Annotated[List[JSONPatch], None]
|
||||
|
||||
VNetsUpdate = Annotated[List[str], None]
|
||||
|
||||
ExtNetsUpdate = Annotated[List[ExtNet], None]
|
||||
|
|
|
@ -171,6 +171,39 @@ async def update_admins(
|
|||
|
||||
return PlainTextResponse(status_code=status.HTTP_200_OK)
|
||||
|
||||
@router.get(
|
||||
"/admins/{objectId}",
|
||||
summary = "Get IPAM Admin",
|
||||
response_model = Admin,
|
||||
status_code = 200
|
||||
)
|
||||
async def get_admins(
|
||||
objectId: UUID = Path(..., description="Azure AD ObjectID for the target user"),
|
||||
authorization: str = Header(None, description="Azure Bearer token"),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Get a specific IPAM admin.
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="API restricted to admins.")
|
||||
|
||||
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
try:
|
||||
admins = copy.deepcopy(admin_query[0])
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="No admins found in database.")
|
||||
|
||||
target_admin = next((x for x in admins['admins'] if x['id'] == str(objectId)), None)
|
||||
|
||||
if target_admin:
|
||||
return target_admin
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Admin not found.")
|
||||
|
||||
@router.delete(
|
||||
"/admins/{objectId}",
|
||||
summary = "Delete IPAM Admin",
|
||||
|
|
|
@ -52,6 +52,8 @@ EXTERNAL_NAME_REGEX = "^(?![\._-])([a-zA-Z0-9\._-]){1,64}(?<![\._-])$"
|
|||
EXTERNAL_DESC_REGEX = "^(?![ /\._-])([a-zA-Z0-9 /\._-]){1,128}(?<![ /\._-])$"
|
||||
EXTSUBNET_NAME_REGEX = "^(?![\._-])([a-zA-Z0-9\._-]){1,64}(?<![\._-])$"
|
||||
EXTSUBNET_DESC_REGEX = "^(?![ /\._-])([a-zA-Z0-9 /\._-]){1,128}(?<![ /\._-])$"
|
||||
EXTENDPOINT_NAME_REGEX = "^(?![\._-])([a-zA-Z0-9\._-]){1,64}(?<![\._-])$"
|
||||
EXTENDPOINT_DESC_REGEX = "^(?![ /\._-])([a-zA-Z0-9 /\._-]){1,128}(?<![ /\._-])$"
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/spaces",
|
||||
|
@ -108,8 +110,7 @@ async def scrub_space_patch(patch, space_name, tenant_id):
|
|||
return scrubbed_patch
|
||||
|
||||
async def valid_block_name_update(name, space_name, block_name, tenant_id):
|
||||
blocks = await cosmos_query("SELECT VALUE LOWER(t.name) FROM c join t IN c.blocks WHERE c.type = 'space' AND LOWER(c.name) != LOWER('{}')".format(space_name), tenant_id)
|
||||
other_blocks = [x for x in blocks if x != block_name.lower()]
|
||||
other_blocks = await cosmos_query("SELECT VALUE LOWER(t.name) FROM c join t IN c.blocks WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) != LOWER('{}')".format(space_name, block_name), tenant_id)
|
||||
|
||||
if name.lower() in other_blocks:
|
||||
raise HTTPException(status_code=400, detail="Updated Block name cannot match existing Blocks within the Space.")
|
||||
|
@ -130,7 +131,10 @@ async def valid_block_cidr_update(cidr, space_name, block_name, tenant_id):
|
|||
if(cidr == target_block['cidr']):
|
||||
return True
|
||||
|
||||
block_network = IPNetwork(cidr)
|
||||
try:
|
||||
block_network = IPNetwork(cidr)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Updated Block CIDR must be in valid CIDR notation (x.x.x.x/x).")
|
||||
|
||||
if(str(block_network.cidr) != cidr):
|
||||
raise HTTPException(status_code=400, detail="Invalid CIDR value, try '{}' instead.".format(block_network.cidr))
|
||||
|
@ -147,6 +151,9 @@ async def valid_block_cidr_update(cidr, space_name, block_name, tenant_id):
|
|||
if target_net:
|
||||
block_cidrs += target_net['prefixes']
|
||||
|
||||
for external in block['externals']:
|
||||
block_cidrs.append(external['cidr'])
|
||||
|
||||
for resv in block['resv']:
|
||||
not resv['settledOn'] and block_cidrs.append(resv['cidr'])
|
||||
|
||||
|
@ -169,14 +176,14 @@ async def scrub_block_patch(patch, space_name, block_name, tenant_id):
|
|||
{
|
||||
"op": "replace",
|
||||
"path": "/name",
|
||||
"valid": valid_block_name_update,
|
||||
"valid": valid_ext_network_name_update,
|
||||
"error": "Block name can be a maximum of 64 characters and may contain alphanumerics, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/cidr",
|
||||
"valid": valid_block_cidr_update,
|
||||
"error": "Block CIDR must be in valid CIDR notation (x.x.x.x/x) and must contain all existing block networks and reservations."
|
||||
"error": "Block CIDR must be in valid CIDR notation (x.x.x.x/x), cannot overlap existing Blocks within the Space and must contain all existing Virtual Networks, External Networks and unfulfilled Reservations within the Block."
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -199,6 +206,300 @@ async def scrub_block_patch(patch, space_name, block_name, tenant_id):
|
|||
|
||||
return scrubbed_patch
|
||||
|
||||
async def valid_ext_network_name_update(name, space_name, block_name, external_name, tenant_id):
|
||||
other_networks = await cosmos_query("SELECT VALUE LOWER(u.name) FROM c join t IN c.blocks join u in t.externals WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}') AND LOWER(u.name) != LOWER('{}')".format(space_name, block_name, external_name), tenant_id)
|
||||
|
||||
if name.lower() in other_networks:
|
||||
raise HTTPException(status_code=400, detail="Updated External Network name cannot match existing External Networks within the Block.")
|
||||
|
||||
if re.match(EXTERNAL_NAME_REGEX, name):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def valid_ext_network_cidr_update(cidr, space_name, block_name, external_name, tenant_id):
|
||||
block_cidrs = []
|
||||
external_cidrs = []
|
||||
|
||||
blocks = await cosmos_query("SELECT VALUE t FROM c join t IN c.blocks WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space_name), tenant_id)
|
||||
target_block = next((x for x in blocks if x['name'].lower() == block_name.lower()), None)
|
||||
|
||||
externals = await cosmos_query("SELECT VALUE u FROM c join t IN c.blocks join u IN t.externals WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}')".format(space_name, block_name), tenant_id)
|
||||
target_external = next((x for x in externals if x['name'].lower() == external_name.lower()), None)
|
||||
|
||||
if target_block and target_external:
|
||||
if(cidr == target_external['cidr']):
|
||||
return True
|
||||
|
||||
try:
|
||||
external_network = IPNetwork(cidr)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Updated External Network CIDR must be in valid CIDR notation (x.x.x.x/x).")
|
||||
|
||||
if(str(external_network.cidr) != cidr):
|
||||
raise HTTPException(status_code=400, detail="Invalid CIDR value, try '{}' instead.".format(external_network.cidr))
|
||||
|
||||
if not external_network in IPNetwork(target_block['cidr']):
|
||||
raise HTTPException(status_code=400, detail="Updated External Network CIDR must be contained within the Block CIDR.")
|
||||
|
||||
net_list = await get_network(None, True)
|
||||
|
||||
for vnet in target_block['vnets']:
|
||||
target_net = next((i for i in net_list if i['id'] == vnet['id']), None)
|
||||
|
||||
if target_net:
|
||||
block_cidrs += target_net['prefixes']
|
||||
|
||||
for resv in target_block['resv']:
|
||||
not resv['settledOn'] and block_cidrs.append(resv['cidr'])
|
||||
|
||||
for external in externals:
|
||||
if external['name'] != external_name:
|
||||
block_cidrs.append(external['cidr'])
|
||||
else:
|
||||
for subnet in external['subnets']:
|
||||
external_cidrs.append(subnet['cidr'])
|
||||
|
||||
update_set = IPSet([cidr])
|
||||
block_set = IPSet(block_cidrs)
|
||||
external_set = IPSet(external_cidrs)
|
||||
|
||||
if block_set & update_set:
|
||||
raise HTTPException(status_code=400, detail="Updated CIDR cannot overlap other Virtual Networks, External Networks, or unfulfilled Reservations within the Block.")
|
||||
|
||||
if not external_set.issubset(update_set):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def scrub_ext_network_patch(patch, space_name, block_name, external_name, tenant_id):
|
||||
scrubbed_patch = []
|
||||
|
||||
allowed_ops = [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/name",
|
||||
"valid": valid_ext_network_name_update,
|
||||
"error": "External Network name can be a maximum of 64 characters and may contain alphanumerics, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/desc",
|
||||
"valid": EXTERNAL_DESC_REGEX,
|
||||
"error": "External Network description can be a maximum of 128 characters and may contain alphanumerics, spaces, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/cidr",
|
||||
"valid": valid_ext_network_cidr_update,
|
||||
"error": "External Network CIDR must be in valid CIDR notation (x.x.x.x/x), must contain all existing External Subnets and cannot overlap existing External Networks, Virtual Networks or unfulfilled Reservations within the Block."
|
||||
}
|
||||
]
|
||||
|
||||
for item in list(patch):
|
||||
target = next((x for x in allowed_ops if (x['op'] == item['op'] and x['path'] == item['path'])), None)
|
||||
|
||||
if target:
|
||||
if isinstance(target['valid'], str):
|
||||
if re.match(target['valid'], str(item['value']), re.IGNORECASE):
|
||||
scrubbed_patch.append(item)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
elif callable(target['valid']):
|
||||
if await target['valid'](item['value'], space_name, block_name, external_name, tenant_id):
|
||||
scrubbed_patch.append(item)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
|
||||
return scrubbed_patch
|
||||
|
||||
async def valid_ext_subnet_name_update(name, space_name, block_name, external_name, subnet_name, tenant_id):
|
||||
other_subnets = await cosmos_query("SELECT VALUE v FROM c join t IN c.blocks join u IN t.externals join v IN u.subnets WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}') AND LOWER(u.name) = LOWER('{}') AND LOWER(v.name) != LOWER('{}')".format(space_name, block_name, external_name, subnet_name), tenant_id)
|
||||
|
||||
if name.lower() in other_subnets:
|
||||
raise HTTPException(status_code=400, detail="Updated External Subnet name cannot match existing External Subnets within the External Network.")
|
||||
|
||||
if re.match(EXTSUBNET_NAME_REGEX, name):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def valid_ext_subnet_cidr_update(cidr, space_name, block_name, external_name, subnet_name, tenant_id):
|
||||
external_cidrs = []
|
||||
subnet_ips = []
|
||||
|
||||
externals = await cosmos_query("SELECT VALUE u FROM c join t IN c.blocks join u IN t.externals WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}')".format(space_name, block_name), tenant_id)
|
||||
target_external = next((x for x in externals if x['name'].lower() == external_name.lower()), None)
|
||||
|
||||
subnets = await cosmos_query("SELECT VALUE v FROM c join t IN c.blocks join u IN t.externals join v IN u.subnets WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}') AND LOWER(u.name) = LOWER('{}')".format(space_name, block_name, external_name), tenant_id)
|
||||
target_subnet = next((x for x in subnets if x['name'].lower() == subnet_name.lower()), None)
|
||||
|
||||
if target_external and target_subnet:
|
||||
if(cidr == target_subnet['cidr']):
|
||||
return True
|
||||
|
||||
try:
|
||||
subnet_network = IPNetwork(cidr)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Updated External Subnet CIDR must be in valid CIDR notation (x.x.x.x/x).")
|
||||
|
||||
if(str(subnet_network.cidr) != cidr):
|
||||
raise HTTPException(status_code=400, detail="Invalid CIDR value, try '{}' instead.".format(subnet_network.cidr))
|
||||
|
||||
if not subnet_network in IPNetwork(target_external['cidr']):
|
||||
raise HTTPException(status_code=400, detail="Updated External Subnet CIDR must be contained within the External Network CIDR.")
|
||||
|
||||
for subnet in subnets:
|
||||
if subnet['name'] != subnet_name:
|
||||
external_cidrs.append(subnet['cidr'])
|
||||
else:
|
||||
for endpoint in subnet['endpoints']:
|
||||
subnet_ips.append(endpoint['ip'])
|
||||
|
||||
update_set = IPSet([cidr])
|
||||
external_set = IPSet(external_cidrs)
|
||||
subnet_set = IPSet(subnet_ips)
|
||||
|
||||
if external_set & update_set:
|
||||
raise HTTPException(status_code=400, detail="Updated CIDR cannot overlap other External Subnets within the External Network.")
|
||||
|
||||
if not subnet_set.issubset(update_set):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def scrub_ext_subnet_patch(patch, space_name, block_name, external_name, subnet_name, tenant_id):
|
||||
scrubbed_patch = []
|
||||
|
||||
allowed_ops = [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/name",
|
||||
"valid": valid_ext_subnet_name_update,
|
||||
"error": "External Subnet name can be a maximum of 64 characters and may contain alphanumerics, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/desc",
|
||||
"valid": EXTSUBNET_DESC_REGEX,
|
||||
"error": "External Subnet description can be a maximum of 128 characters and may contain alphanumerics, spaces, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/cidr",
|
||||
"valid": valid_ext_subnet_cidr_update,
|
||||
"error": "External Subnet CIDR must be in valid CIDR notation (x.x.x.x/x), must contain all existing Endpoints and cannot overlap existing External Subnets within the External Network."
|
||||
}
|
||||
]
|
||||
|
||||
for item in list(patch):
|
||||
target = next((x for x in allowed_ops if (x['op'] == item['op'] and x['path'] == item['path'])), None)
|
||||
|
||||
if target:
|
||||
if isinstance(target['valid'], str):
|
||||
if re.match(target['valid'], str(item['value']), re.IGNORECASE):
|
||||
scrubbed_patch.append(item)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
elif callable(target['valid']):
|
||||
if await target['valid'](item['value'], space_name, block_name, external_name, subnet_name, tenant_id):
|
||||
scrubbed_patch.append(item)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
|
||||
return scrubbed_patch
|
||||
|
||||
async def valid_ext_endpoint_name_update(name, space_name, block_name, external_name, subnet_name, endpoint_name, tenant_id):
|
||||
other_endpoints = await cosmos_query("SELECT VALUE x FROM c join t IN c.blocks join u IN t.externals join v IN u.subnets join x in v.endpoints WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}') AND LOWER(u.name) = LOWER('{}') AND LOWER(v.name) = LOWER('{}') AND LOWER(x.name) != LOWER('{}')".format(space_name, block_name, external_name, subnet_name, endpoint_name), tenant_id)
|
||||
|
||||
if name.lower() in other_endpoints:
|
||||
raise HTTPException(status_code=400, detail="Updated External Endpoint name cannot match existing External Endpoints within the External Subnet.")
|
||||
|
||||
if re.match(EXTENDPOINT_NAME_REGEX, name):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def valid_ext_endpoint_ip_update(ip, space_name, block_name, external_name, subnet_name, endpoint_name, tenant_id):
|
||||
subnet_ips = []
|
||||
|
||||
subnets = await cosmos_query("SELECT VALUE v FROM c join t IN c.blocks join u IN t.externals join v IN u.subnets WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}') AND LOWER(u.name) = LOWER('{}')".format(space_name, block_name, external_name), tenant_id)
|
||||
target_subnet = next((x for x in subnets if x['name'].lower() == subnet_name.lower()), None)
|
||||
|
||||
endpoints = await cosmos_query("SELECT VALUE x FROM c join t IN c.blocks join u IN t.externals join v IN u.subnets join x in v.endpoints WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}') AND LOWER(t.name) = LOWER('{}') AND LOWER(u.name) = LOWER('{}') and LOWER(v.name) = LOWER('{}')".format(space_name, block_name, external_name, subnet_name), tenant_id)
|
||||
target_endpoint = next((x for x in endpoints if x['name'].lower() == endpoint_name.lower()), None)
|
||||
|
||||
if target_subnet and target_endpoint:
|
||||
if(ip == target_endpoint['ip']):
|
||||
return True
|
||||
|
||||
try:
|
||||
endpoint_ip = IPAddress(ip)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Updated External Endpoint IP must be in valid IPv4 notation (x.x.x.x).")
|
||||
|
||||
if not endpoint_ip in IPNetwork(target_subnet['cidr']):
|
||||
raise HTTPException(status_code=400, detail="Updated External Endpoint IP must be contained within the External Subnet CIDR.")
|
||||
|
||||
for endpoint in endpoints:
|
||||
if endpoint['name'] != endpoint_name:
|
||||
subnet_ips.append(endpoint['ip'])
|
||||
|
||||
update_set = IPSet([ip])
|
||||
subnet_set = IPSet(subnet_ips)
|
||||
|
||||
if subnet_set & update_set:
|
||||
raise HTTPException(status_code=400, detail="Updated IP cannot overlap other External Endpoints within the External Subnet.")
|
||||
|
||||
return True
|
||||
|
||||
async def scrub_ext_endpoint_patch(patch, space_name, block_name, external_name, subnet_name, endpoint_name, tenant_id):
|
||||
scrubbed_patch = []
|
||||
|
||||
allowed_ops = [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/name",
|
||||
"valid": valid_ext_endpoint_name_update,
|
||||
"error": "External Endpoint name can be a maximum of 64 characters and may contain alphanumerics, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/desc",
|
||||
"valid": EXTENDPOINT_DESC_REGEX,
|
||||
"error": "External Endpoint description can be a maximum of 128 characters and may contain alphanumerics, spaces, underscores, hypens, slashes, and periods."
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/ip",
|
||||
"valid": valid_ext_endpoint_ip_update,
|
||||
"error": "External Endpoint IP must be in valid IPv4 notation (x.x.x.x) and cannot overlap existing External Endpoints within the External Subnet."
|
||||
}
|
||||
]
|
||||
|
||||
for item in list(patch):
|
||||
target = next((x for x in allowed_ops if (x['op'] == item['op'] and x['path'] == item['path'])), None)
|
||||
|
||||
if target:
|
||||
if isinstance(target['valid'], str):
|
||||
if re.match(target['valid'], str(item['value']), re.IGNORECASE):
|
||||
scrubbed_patch.append(item)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
elif callable(target['valid']):
|
||||
if await target['valid'](item['value'], space_name, block_name, external_name, subnet_name, endpoint_name, tenant_id):
|
||||
scrubbed_patch.append(item)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=target['error'])
|
||||
|
||||
return scrubbed_patch
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
summary = "Get All Spaces",
|
||||
|
@ -1602,6 +1903,72 @@ async def get_external_network(
|
|||
|
||||
return target_ext_network
|
||||
|
||||
@router.patch(
|
||||
"/{space}/blocks/{block}/externals/{external}",
|
||||
summary = "Update External Network Details",
|
||||
response_model = ExtNet,
|
||||
status_code = 200
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error updating external network, please try again."
|
||||
)
|
||||
async def update_ext_network(
|
||||
updates: ExtNetUpdate,
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
external: str = Path(..., description="Name of the target External Network"),
|
||||
authorization: str = Header(None, description="Azure Bearer token"),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Update an External Network with a JSON patch:
|
||||
|
||||
- **[<JSON Patch>]**: Array of JSON Patches
|
||||
|
||||
Allowed operations:
|
||||
- **replace**
|
||||
|
||||
Allowed paths:
|
||||
- **/name**
|
||||
- **/desc**
|
||||
- **/cidr**
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="This API is admin restricted.")
|
||||
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
|
||||
|
||||
try:
|
||||
target_space = copy.deepcopy(space_query[0])
|
||||
update_space = copy.deepcopy(space_query[0])
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid space name.")
|
||||
|
||||
target_block = next((x for x in update_space['blocks'] if x['name'].lower() == block.lower()), None)
|
||||
|
||||
if not target_block:
|
||||
raise HTTPException(status_code=400, detail="Invalid block name.")
|
||||
|
||||
update_ext_network = next((x for x in target_block['externals'] if x['name'].lower() == external.lower()), None)
|
||||
|
||||
if not update_ext_network:
|
||||
raise HTTPException(status_code=400, detail="Invalid external network name.")
|
||||
|
||||
try:
|
||||
patch = jsonpatch.JsonPatch([x.model_dump() for x in updates])
|
||||
except jsonpatch.InvalidJsonPatch:
|
||||
raise HTTPException(status_code=500, detail="Invalid JSON patch, please review and try again.")
|
||||
|
||||
scrubbed_patch = jsonpatch.JsonPatch(await scrub_ext_network_patch(patch, space, block, external, tenant_id))
|
||||
scrubbed_patch.apply(update_ext_network, in_place=True)
|
||||
|
||||
await cosmos_replace(target_space, update_space)
|
||||
|
||||
return update_ext_network
|
||||
|
||||
@router.delete(
|
||||
"/{space}/blocks/{block}/externals/{external}",
|
||||
summary = "Remove External Network",
|
||||
|
@ -1842,6 +2209,78 @@ async def get_external_subnet(
|
|||
|
||||
return target_ext_subnet
|
||||
|
||||
@router.patch(
|
||||
"/{space}/blocks/{block}/externals/{external}/subnets/{subnet}",
|
||||
summary = "Update External Subnet Details",
|
||||
response_model = ExtSubnet,
|
||||
status_code = 200
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error updating external subnet, please try again."
|
||||
)
|
||||
async def update_ext_subnet(
|
||||
updates: ExtSubnetUpdate,
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
external: str = Path(..., description="Name of the target External Network"),
|
||||
subnet: str = Path(..., description="Name of the target external subnet"),
|
||||
authorization: str = Header(None, description="Azure Bearer token"),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Update an External Subnet with a JSON patch:
|
||||
|
||||
- **[<JSON Patch>]**: Array of JSON Patches
|
||||
|
||||
Allowed operations:
|
||||
- **replace**
|
||||
|
||||
Allowed paths:
|
||||
- **/name**
|
||||
- **/desc**
|
||||
- **/cidr**
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="This API is admin restricted.")
|
||||
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
|
||||
|
||||
try:
|
||||
target_space = copy.deepcopy(space_query[0])
|
||||
update_space = copy.deepcopy(space_query[0])
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid space name.")
|
||||
|
||||
target_block = next((x for x in update_space['blocks'] if x['name'].lower() == block.lower()), None)
|
||||
|
||||
if not target_block:
|
||||
raise HTTPException(status_code=400, detail="Invalid block name.")
|
||||
|
||||
external_network = next((x for x in target_block['externals'] if x['name'].lower() == external.lower()), None)
|
||||
|
||||
if not external_network:
|
||||
raise HTTPException(status_code=400, detail="Invalid external network name.")
|
||||
|
||||
update_ext_subnet = next((x for x in external_network['subnets'] if x['name'].lower() == subnet.lower()), None)
|
||||
|
||||
if not update_ext_subnet:
|
||||
raise HTTPException(status_code=400, detail="Invalid external subnet name.")
|
||||
|
||||
try:
|
||||
patch = jsonpatch.JsonPatch([x.model_dump() for x in updates])
|
||||
except jsonpatch.InvalidJsonPatch:
|
||||
raise HTTPException(status_code=500, detail="Invalid JSON patch, please review and try again.")
|
||||
|
||||
scrubbed_patch = jsonpatch.JsonPatch(await scrub_ext_subnet_patch(patch, space, block, external, subnet, tenant_id))
|
||||
scrubbed_patch.apply(update_ext_subnet, in_place=True)
|
||||
|
||||
await cosmos_replace(target_space, update_space)
|
||||
|
||||
return update_ext_subnet
|
||||
|
||||
@router.delete(
|
||||
"/{space}/blocks/{block}/externals/{external}/subnets/{subnet}",
|
||||
summary = "Remove External Network Subnet",
|
||||
|
@ -1957,7 +2396,7 @@ async def get_external_subnet_endpoints(
|
|||
error_msg = "Error creating external network subnet endpoint, please try again."
|
||||
)
|
||||
async def create_external_subnet_endpoint(
|
||||
endpoint: ExtEndpointUpdate,
|
||||
endpoint: ExtEndpointReq,
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
external: str = Path(..., description="Name of the target External Network"),
|
||||
|
@ -2005,10 +2444,10 @@ async def create_external_subnet_endpoint(
|
|||
if endpoint_name_overlap:
|
||||
raise HTTPException(status_code=400, detail="Target endpoint name overlaps existing endpoint name.")
|
||||
|
||||
if not re.match(EXTERNAL_NAME_REGEX, endpoint.name, re.IGNORECASE):
|
||||
if not re.match(EXTENDPOINT_NAME_REGEX, endpoint.name, re.IGNORECASE):
|
||||
raise HTTPException(status_code=400, detail="Endpoint names can be a maximum of 32 characters and may contain alphanumerics, underscores, hypens, and periods.")
|
||||
|
||||
if not re.match(EXTERNAL_DESC_REGEX, endpoint.desc, re.IGNORECASE):
|
||||
if not re.match(EXTENDPOINT_DESC_REGEX, endpoint.desc, re.IGNORECASE):
|
||||
raise HTTPException(status_code=400, detail="Endpoint descriptions can be a maximum of 64 characters and may contain alphanumerics, spaces, underscores, hypens, slashes, and periods.")
|
||||
|
||||
subnet_network = IPNetwork(target_ext_subnet['cidr'])
|
||||
|
@ -2052,7 +2491,7 @@ async def create_external_subnet_endpoint(
|
|||
error_msg = "Error updating external network subnet endpoints, please try again."
|
||||
)
|
||||
async def update_external_subnet_enpoints(
|
||||
endpoints: List[ExtEndpointUpdate],
|
||||
endpoints: List[ExtEndpointReq],
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
external: str = Path(..., description="Name of the target External Network"),
|
||||
|
@ -2086,10 +2525,10 @@ async def update_external_subnet_enpoints(
|
|||
invalid_descs = []
|
||||
|
||||
for endpoint in endpoints:
|
||||
if not re.match(EXTERNAL_NAME_REGEX, endpoint.name, re.IGNORECASE):
|
||||
if not re.match(EXTENDPOINT_NAME_REGEX, endpoint.name, re.IGNORECASE):
|
||||
invalid_names.append(endpoint['name'])
|
||||
|
||||
if not re.match(EXTERNAL_DESC_REGEX, endpoint.desc, re.IGNORECASE):
|
||||
if not re.match(EXTENDPOINT_DESC_REGEX, endpoint.desc, re.IGNORECASE):
|
||||
invalid_descs.append(endpoint['desc'])
|
||||
|
||||
if invalid_names:
|
||||
|
@ -2282,6 +2721,84 @@ async def get_external_subnet_endpoint(
|
|||
|
||||
return target_ext_endpoint
|
||||
|
||||
@router.patch(
|
||||
"/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints/{endpoint}",
|
||||
summary = "Update External Endpoint Details",
|
||||
response_model = ExtEndpoint,
|
||||
status_code = 200
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error updating external endpoint, please try again."
|
||||
)
|
||||
async def update_ext_endpoint(
|
||||
updates: ExtEndpointUpdate,
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
external: str = Path(..., description="Name of the target External Network"),
|
||||
subnet: str = Path(..., description="Name of the target external subnet"),
|
||||
endpoint: str = Path(..., description="Name of the target external subnet endpoint"),
|
||||
authorization: str = Header(None, description="Azure Bearer token"),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Update an External Endpoint with a JSON patch:
|
||||
|
||||
- **[<JSON Patch>]**: Array of JSON Patches
|
||||
|
||||
Allowed operations:
|
||||
- **replace**
|
||||
|
||||
Allowed paths:
|
||||
- **/name**
|
||||
- **/desc**
|
||||
- **/ip**
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="This API is admin restricted.")
|
||||
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
|
||||
|
||||
try:
|
||||
target_space = copy.deepcopy(space_query[0])
|
||||
update_space = copy.deepcopy(space_query[0])
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid space name.")
|
||||
|
||||
target_block = next((x for x in update_space['blocks'] if x['name'].lower() == block.lower()), None)
|
||||
|
||||
if not target_block:
|
||||
raise HTTPException(status_code=400, detail="Invalid block name.")
|
||||
|
||||
external_network = next((x for x in target_block['externals'] if x['name'].lower() == external.lower()), None)
|
||||
|
||||
if not external_network:
|
||||
raise HTTPException(status_code=400, detail="Invalid external network name.")
|
||||
|
||||
external_subnet = next((x for x in external_network['subnets'] if x['name'].lower() == subnet.lower()), None)
|
||||
|
||||
if not external_subnet:
|
||||
raise HTTPException(status_code=400, detail="Invalid external subnet name.")
|
||||
|
||||
update_ext_endpoint = next((x for x in external_subnet['endpoints'] if x['name'].lower() == endpoint.lower()), None)
|
||||
|
||||
if not update_ext_endpoint:
|
||||
raise HTTPException(status_code=400, detail="Invalid external endpoint name.")
|
||||
|
||||
try:
|
||||
patch = jsonpatch.JsonPatch([x.model_dump() for x in updates])
|
||||
except jsonpatch.InvalidJsonPatch:
|
||||
raise HTTPException(status_code=500, detail="Invalid JSON patch, please review and try again.")
|
||||
|
||||
scrubbed_patch = jsonpatch.JsonPatch(await scrub_ext_endpoint_patch(patch, space, block, external, subnet, endpoint, tenant_id))
|
||||
scrubbed_patch.apply(update_ext_endpoint, in_place=True)
|
||||
|
||||
await cosmos_replace(target_space, update_space)
|
||||
|
||||
return update_ext_endpoint
|
||||
|
||||
@router.delete(
|
||||
"/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints/{endpoint}",
|
||||
summary = "Remove External Network Subnet Endpoint",
|
||||
|
@ -2618,3 +3135,107 @@ async def delete_block_reservations(
|
|||
await cosmos_replace(space_query[0], target_space)
|
||||
|
||||
return PlainTextResponse(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@router.get(
|
||||
"/{space}/blocks/{block}/reservations/{reservation}",
|
||||
summary = "Get Block Reservation",
|
||||
response_model = ReservationExpand,
|
||||
status_code = 200
|
||||
)
|
||||
async def get_block_reservations(
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
reservation: str = Path(..., description="ID of the target Reservation"),
|
||||
authorization: str = Header(None, description="Azure Bearer token"),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Get the details of a specific CIDR Reservation.
|
||||
"""
|
||||
|
||||
user_assertion = authorization.split(' ')[1]
|
||||
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
|
||||
|
||||
try:
|
||||
target_space = copy.deepcopy(space_query[0])
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid space name.")
|
||||
|
||||
target_block = next((x for x in target_space['blocks'] if x['name'].lower() == block.lower()), None)
|
||||
|
||||
if not target_block:
|
||||
raise HTTPException(status_code=400, detail="Invalid block name.")
|
||||
|
||||
target_reservation = next((x for x in target_block['resv'] if x['id'] == reservation), None)
|
||||
|
||||
if not target_reservation:
|
||||
raise HTTPException(status_code=400, detail="Invalid reservation ID.")
|
||||
|
||||
target_reservation['space'] = target_space['name']
|
||||
target_reservation['block'] = target_block['name']
|
||||
|
||||
if not is_admin:
|
||||
user_name = get_username_from_jwt(user_assertion)
|
||||
|
||||
if target_reservation['createdBy'] == user_name:
|
||||
return target_reservation
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="Users can only view their own reservations.")
|
||||
else:
|
||||
return target_reservation
|
||||
|
||||
@router.delete(
|
||||
"/{space}/blocks/{block}/reservations/{reservation}",
|
||||
summary = "Delete CIDR Reservation",
|
||||
status_code = 204
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error removing reservation, please try again."
|
||||
)
|
||||
async def delete_block_reservations(
|
||||
space: str = Path(..., description="Name of the target Space"),
|
||||
block: str = Path(..., description="Name of the target Block"),
|
||||
reservation: str = Path(..., description="ID of the target Reservation"),
|
||||
authorization: str = Header(None, description="Azure Bearer token"),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Remove a specific CIDR Reservation.
|
||||
"""
|
||||
|
||||
user_assertion = authorization.split(' ')[1]
|
||||
user_name = get_username_from_jwt(user_assertion)
|
||||
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
|
||||
|
||||
try:
|
||||
target_space = copy.deepcopy(space_query[0])
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid space name.")
|
||||
|
||||
target_block = next((x for x in target_space['blocks'] if x['name'].lower() == block.lower()), None)
|
||||
|
||||
if not target_block:
|
||||
raise HTTPException(status_code=400, detail="Invalid block name.")
|
||||
|
||||
target_reservation = next((x for x in target_block['resv'] if x['id'] == reservation), None)
|
||||
|
||||
if not target_reservation:
|
||||
raise HTTPException(status_code=400, detail="Invalid reservation ID.")
|
||||
|
||||
if not is_admin:
|
||||
if target_reservation['createdBy'] != user_name:
|
||||
raise HTTPException(status_code=403, detail="Users can only delete their own reservations.")
|
||||
|
||||
if not target_reservation['settledOn']:
|
||||
target_reservation['settledOn'] = time.time()
|
||||
target_reservation['settledBy'] = user_name
|
||||
target_reservation['status'] = "cancelledByUser"
|
||||
|
||||
await cosmos_replace(space_query[0], target_space)
|
||||
|
||||
return PlainTextResponse(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
@ -317,7 +317,7 @@ Context 'Blocks' {
|
|||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}
|
||||
It 'Get A Specific Block' {
|
||||
It 'Get a Specific Block' {
|
||||
|
||||
$block, $blockStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA'
|
||||
|
||||
|
@ -336,7 +336,7 @@ Context 'Networks' {
|
|||
}
|
||||
|
||||
# POST /api/spaces/{space}/blocks/{block}/networks
|
||||
It 'Add a Virtual Network to a Block' {
|
||||
It 'Add a Virtual Network to Block' {
|
||||
$script:newNetA = New-AzVirtualNetwork `
|
||||
-Name 'TestVNet01' `
|
||||
-ResourceGroupName $env:IPAM_RESOURCE_GROUP `
|
||||
|
@ -446,7 +446,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}/externals/{external}
|
||||
It 'Get Specific External Network' {
|
||||
It 'Get a Specific External Network' {
|
||||
|
||||
$external, $externalStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetB'
|
||||
|
||||
|
@ -455,9 +455,44 @@ Context 'External Networks' {
|
|||
$external.Cidr -eq "10.1.2.0/24" | Should -Be $true
|
||||
}
|
||||
|
||||
# PATCH /api/spaces/{space}/blocks/{block}/externals/{external}
|
||||
It 'Update an External Network' {
|
||||
$update = @(
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/name'
|
||||
value = 'ExternalNetC'
|
||||
}
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/desc'
|
||||
value = 'External Network C'
|
||||
}
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/cidr'
|
||||
value = '10.1.3.0/23'
|
||||
}
|
||||
)
|
||||
|
||||
Update-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetB' $update
|
||||
|
||||
$externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals'
|
||||
|
||||
$externals.Count | Should -Be 2
|
||||
|
||||
$externals[0].Name -eq "ExternalNetA" | Should -Be $true
|
||||
$externals[0].Desc -eq "External Network A" | Should -Be $true
|
||||
$externals[0].Cidr -eq "10.1.1.0/24" | Should -Be $true
|
||||
|
||||
$externals[1].Name -eq "ExternalNetC" | Should -Be $true
|
||||
$externals[1].Desc -eq "External Network B" | Should -Be $true
|
||||
$externals[1].Cidr -eq "10.1.3.0/23" | Should -Be $true
|
||||
}
|
||||
|
||||
# DELETE /api/spaces/{space}/blocks/{block}/externals/{external}
|
||||
It 'Delete an External Network' {
|
||||
Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetB'
|
||||
Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetC'
|
||||
|
||||
$externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals'
|
||||
|
||||
|
@ -469,7 +504,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}/externals/{external}/subnets
|
||||
It 'Verify No Subnets Exist in External Network' {
|
||||
It 'Verify No External Subnets Exist in External Network' {
|
||||
|
||||
$subnets, $subnetsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets'
|
||||
|
||||
|
@ -477,7 +512,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# POST /api/spaces/{space}/blocks/{block}/externals/{external}/subnets
|
||||
It 'Add a Subnet to an External Network' {
|
||||
It 'Add an External Subnet to an External Network' {
|
||||
$script:subnetA = @{
|
||||
name = "SubnetA"
|
||||
desc = "Subnet A"
|
||||
|
@ -496,7 +531,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# POST /api/spaces/{space}/blocks/{block}/externals/{external}/subnets
|
||||
It 'Add a Second Subnet to an External Network' {
|
||||
It 'Add a Second External Subnet to an External Network' {
|
||||
$script:subnetB = @{
|
||||
name = "SubnetB"
|
||||
desc = "Subnet B"
|
||||
|
@ -519,7 +554,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}
|
||||
It 'Get Specific Subnet' {
|
||||
It 'Get Specific External Subnet' {
|
||||
|
||||
$subnet, $subnetStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetB'
|
||||
|
||||
|
@ -528,9 +563,44 @@ Context 'External Networks' {
|
|||
$subnet.Cidr -eq "10.1.1.64/26" | Should -Be $true
|
||||
}
|
||||
|
||||
# PATCH /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}
|
||||
It 'Update an External Subnet' {
|
||||
$update = @(
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/name'
|
||||
value = 'SubnetC'
|
||||
}
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/desc'
|
||||
value = 'Subnet C'
|
||||
}
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/cidr'
|
||||
value = '10.1.1.128/27'
|
||||
}
|
||||
)
|
||||
|
||||
Update-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetB' $update
|
||||
|
||||
$subnets, $subnetsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets'
|
||||
|
||||
$subnets.Count | Should -Be 2
|
||||
|
||||
$subnets[0].Name -eq "SubnetA" | Should -Be $true
|
||||
$subnets[0].Desc -eq "Subnet A" | Should -Be $true
|
||||
$subnets[0].Cidr -eq "10.1.1.0/26" | Should -Be $true
|
||||
|
||||
$subnets[1].Name -eq "SubnetC" | Should -Be $true
|
||||
$subnets[1].Desc -eq "Subnet C" | Should -Be $true
|
||||
$subnets[1].Cidr -eq "10.1.1.128/27" | Should -Be $true
|
||||
}
|
||||
|
||||
# DELETE /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}
|
||||
It 'Delete an Subnet' {
|
||||
Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetB'
|
||||
It 'Delete an External Subnet' {
|
||||
Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetC'
|
||||
|
||||
$subnets, $subnetsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets'
|
||||
|
||||
|
@ -542,7 +612,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints
|
||||
It 'Verify No Endpoints Exist in Subnet' {
|
||||
It 'Verify No External Endpoints Exist in External Subnet' {
|
||||
|
||||
$endpoints, $endpointsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints'
|
||||
|
||||
|
@ -550,7 +620,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# POST /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints
|
||||
It 'Add an Endpoint to a Subnet' {
|
||||
It 'Add an External Endpoint to an External Subnet' {
|
||||
$script:endpointA = @{
|
||||
name = "EndpointA"
|
||||
desc = "Endpoint A"
|
||||
|
@ -569,7 +639,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# POST /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints
|
||||
It 'Add a Second Endpoint to a Subnet' {
|
||||
It 'Add a Second External Endpoint to an External Subnet' {
|
||||
$script:endpointB = @{
|
||||
name = "EndpointB"
|
||||
desc = "Endpoint B"
|
||||
|
@ -592,7 +662,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# PUT /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints
|
||||
It 'Replace Endpoints in Subnet' {
|
||||
It 'Replace External Endpoints in an External Subnet' {
|
||||
$script:endpointC = @{
|
||||
name = "EndpointC"
|
||||
desc = "Endpoint C"
|
||||
|
@ -636,7 +706,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# DELETE /api/spaces/{space}/blocks/{block}/externals/ExternalNetA/subnets/SubnetA/endpoints
|
||||
It 'Delete Endpoint' {
|
||||
It 'Delete External Endpoints' {
|
||||
$body = @(
|
||||
$script:endpointC.name
|
||||
$script:endpointD.name
|
||||
|
@ -658,7 +728,7 @@ Context 'External Networks' {
|
|||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints/{endpoint}
|
||||
It 'Get Specific Endpoint' {
|
||||
It 'Get a Specific External Endpoint' {
|
||||
|
||||
$endpoint, $endpointStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints/EndpointA'
|
||||
|
||||
|
@ -667,17 +737,52 @@ Context 'External Networks' {
|
|||
$endpoint.IP | Should -Be "10.1.1.4"
|
||||
}
|
||||
|
||||
# PATCH /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints/{endpoint}
|
||||
It 'Update an External Endpoint' {
|
||||
$update = @(
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/name'
|
||||
value = 'EndpointC'
|
||||
}
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/desc'
|
||||
value = 'Endpoint C'
|
||||
}
|
||||
@{
|
||||
op = 'replace'
|
||||
path = '/ip'
|
||||
value = '10.1.1.10'
|
||||
}
|
||||
)
|
||||
|
||||
Update-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints/EndpointB' $update
|
||||
|
||||
$endpoints, $endpointsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints'
|
||||
|
||||
$endpoints.Count | Should -Be 2
|
||||
|
||||
$endpoints[0].Name -eq "EndpointA" | Should -Be $true
|
||||
$endpoints[0].Desc -eq "Endpoint A" | Should -Be $true
|
||||
$endpoints[0].IP -eq "10.1.1.4" | Should -Be $true
|
||||
|
||||
$endpoints[1].Name -eq "EndpointC" | Should -Be $true
|
||||
$endpoints[1].Desc -eq "Endpoint C" | Should -Be $true
|
||||
$endpoints[1].IP -eq "10.1.1.10" | Should -Be $true
|
||||
}
|
||||
|
||||
# DELETE /api/spaces/{space}/blocks/{block}/externals/{external}/subnets/{subnet}/endpoints/{endpoint}
|
||||
It 'Delete an Endpoint' {
|
||||
Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints/EndpointA'
|
||||
It 'Delete an External Endpoint' {
|
||||
Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints/EndpointC'
|
||||
|
||||
$endpoints, $endpointsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetA/subnets/SubnetA/endpoints'
|
||||
|
||||
$endpoints.Count | Should -Be 1
|
||||
|
||||
$endpoints[0].Name -eq "EndpointB" | Should -Be $true
|
||||
$endpoints[0].Desc -eq "Endpoint B" | Should -Be $true
|
||||
$endpoints[0].IP -eq "10.1.1.1" | Should -Be $true
|
||||
$endpoints[0].Name -eq "EndpointA" | Should -Be $true
|
||||
$endpoints[0].Desc -eq "Endpoint A" | Should -Be $true
|
||||
$endpoints[0].IP -eq "10.1.1.4" | Should -Be $true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -702,11 +807,18 @@ Context 'Reservations' {
|
|||
desc = "Test Reservation B"
|
||||
}
|
||||
|
||||
$bodyC = @{
|
||||
size = 24
|
||||
desc = "Test Reservation C"
|
||||
}
|
||||
|
||||
$script:reservationA, $reservationAStatus = New-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $bodyA
|
||||
$script:reservationB, $reservationBStatus = New-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $bodyB
|
||||
$script:reservationC, $reservationCStatus = New-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $bodyC
|
||||
|
||||
$reservations, $reservationsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations'
|
||||
|
||||
$reservations.Count | Should -Be 2
|
||||
$reservations.Count | Should -Be 3
|
||||
|
||||
$reservations[0].Space -eq "TestSpaceA" | Should -Be $true
|
||||
$reservations[0].Block -eq "TestBlockA" | Should -Be $true
|
||||
|
@ -719,6 +831,12 @@ Context 'Reservations' {
|
|||
$reservations[1].Desc -eq "Test Reservation B" | Should -Be $true
|
||||
$reservations[1].Cidr -eq "10.1.3.0/24" | Should -Be $true
|
||||
$reservations[1].SettledOn -eq $null | Should -Be $true
|
||||
|
||||
$reservations[2].Space -eq "TestSpaceA" | Should -Be $true
|
||||
$reservations[2].Block -eq "TestBlockA" | Should -Be $true
|
||||
$reservations[2].Desc -eq "Test Reservation C" | Should -Be $true
|
||||
$reservations[2].Cidr -eq "10.1.4.0/24" | Should -Be $true
|
||||
$reservations[2].SettledOn -eq $null | Should -Be $true
|
||||
}
|
||||
|
||||
# Create an Azure Virtual Network w/ Reservation ID Tag and Verify it's Automatically Imported into IPAM
|
||||
|
@ -742,16 +860,18 @@ Context 'Reservations' {
|
|||
$($networks | Select-Object -ExpandProperty id) -contains $script:newNetA.Id | Should -Be $true
|
||||
$($networks | Select-Object -ExpandProperty id) -contains $script:newNetC.Id | Should -Be $true
|
||||
|
||||
$reservations.Count | Should -Be 2
|
||||
$reservations.Count | Should -Be 3
|
||||
|
||||
$reservations[0].SettledOn -eq $null | Should -Be $false
|
||||
$reservations[0].Status -eq "fulfilled" | Should -Be $true
|
||||
$reservations[1].SettledOn -eq $null | Should -Be $true
|
||||
$reservations[1].Status -eq "wait" | Should -Be $true
|
||||
$reservations[2].SettledOn -eq $null | Should -Be $true
|
||||
$reservations[2].Status -eq "wait" | Should -Be $true
|
||||
}
|
||||
|
||||
# DELETE /api/spaces/{space}/blocks/{block}/reservations
|
||||
It 'Delete A Reservation' {
|
||||
It 'Delete Reservations' {
|
||||
$body = @(
|
||||
$script:reservationB.Id
|
||||
)
|
||||
|
@ -764,12 +884,46 @@ Context 'Reservations' {
|
|||
|
||||
$reservations, $reservationsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $query
|
||||
|
||||
$reservations.Count | Should -Be 2
|
||||
$reservations.Count | Should -Be 3
|
||||
|
||||
$reservations[0].SettledOn -eq $null | Should -Be $false
|
||||
$reservations[0].Status -eq "fulfilled" | Should -Be $true
|
||||
$reservations[1].SettledOn -eq $null | Should -Be $false
|
||||
$reservations[1].Status -eq "cancelledByUser" | Should -Be $true
|
||||
$reservations[2].SettledOn -eq $null | Should -Be $true
|
||||
$reservations[2].Status -eq "wait" | Should -Be $true
|
||||
}
|
||||
|
||||
# GET /api/spaces/{space}/blocks/{block}/reservations/{reservationId}
|
||||
It 'Get a Specific Reservation' {
|
||||
|
||||
$reservation, $reservationStatus = Get-ApiResource "/spaces/TestSpaceA/blocks/TestBlockA/reservations/$($script:reservationA.Id)"
|
||||
|
||||
$reservation.Space -eq "TestSpaceA" | Should -Be $true
|
||||
$reservation.Block -eq "TestBlockA" | Should -Be $true
|
||||
$reservation.Desc -eq "Test Reservation C" | Should -Be $true
|
||||
$reservation.Cidr -eq "10.1.4.0/24" | Should -Be $true
|
||||
$reservation.SettledOn -eq $null | Should -Be $true
|
||||
}
|
||||
|
||||
# DELETE /api/spaces/{space}/blocks/{block}/reservations/{reservationId}
|
||||
It 'Delete a Specific Reservation' {
|
||||
$query = @{
|
||||
settled = $true
|
||||
}
|
||||
|
||||
Remove-ApiResource "/spaces/TestSpaceA/blocks/TestBlockA/reservations/$($script:reservationA.Id)"
|
||||
|
||||
$reservations, $reservationsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $query
|
||||
|
||||
$reservations.Count | Should -Be 3
|
||||
|
||||
$reservations[0].SettledOn -eq $null | Should -Be $false
|
||||
$reservations[0].Status -eq "fulfilled" | Should -Be $true
|
||||
$reservations[1].SettledOn -eq $null | Should -Be $false
|
||||
$reservations[1].Status -eq "cancelledByUser" | Should -Be $true
|
||||
$reservations[2].SettledOn -eq $null | Should -Be $false
|
||||
$reservations[2].Status -eq "cancelledByUser" | Should -Be $true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -4,22 +4,23 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^3.16.0",
|
||||
"@azure/msal-react": "^2.0.18",
|
||||
"@azure/msal-browser": "^3.17.0",
|
||||
"@azure/msal-react": "^2.0.19",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@inovua/reactdatagrid-community": "^5.10.2",
|
||||
"@mui/icons-material": "^5.15.19",
|
||||
"@mui/icons-material": "^5.15.21",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.19",
|
||||
"@mui/material": "^5.15.21",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/jest-dom": "^6.4.6",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"axios": "^1.7.2",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts": "^5.5.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"notistack": "^3.0.1",
|
||||
"pluralize": "^8.0.0",
|
||||
|
@ -27,9 +28,9 @@
|
|||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-router-dom": "^6.24.0",
|
||||
"spinners-react": "^1.0.7",
|
||||
"web-vitals": "^4.0.1"
|
||||
"web-vitals": "^4.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -48,12 +49,12 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"serve": "^14.2.3",
|
||||
"vite": "^5.2.12",
|
||||
"vite": "^5.3.2",
|
||||
"vite-plugin-eslint2": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
TaskAltOutlined,
|
||||
CancelOutlined,
|
||||
AddOutlined,
|
||||
// EditOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutline
|
||||
} from "@mui/icons-material";
|
||||
|
||||
|
@ -39,6 +39,7 @@ import {
|
|||
} from "../../../ipam/ipamSlice";
|
||||
|
||||
import AddExtNetwork from "./utils/addNetwork";
|
||||
import EditExtNetwork from "./utils/editNetwork";
|
||||
import DeleteExtNetwork from "./utils/deleteNetwork";
|
||||
|
||||
import { ExternalContext } from "../externalContext";
|
||||
|
@ -58,6 +59,7 @@ function HeaderMenu(props) {
|
|||
selectedBlock,
|
||||
selectedExternal,
|
||||
setAddExtOpen,
|
||||
setEditExtOpen,
|
||||
setDelExtOpen,
|
||||
saving,
|
||||
sendResults,
|
||||
|
@ -81,6 +83,11 @@ function HeaderMenu(props) {
|
|||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
const onEditExt = () => {
|
||||
setEditExtOpen(true);
|
||||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
const onDelExt = () => {
|
||||
setDelExtOpen(true);
|
||||
setMenuOpen(false);
|
||||
|
@ -183,15 +190,15 @@ function HeaderMenu(props) {
|
|||
</ListItemIcon>
|
||||
Add Network
|
||||
</MenuItem>
|
||||
{/* <MenuItem
|
||||
onClick={() => console.log("EDIT!")}
|
||||
disabled={ true }
|
||||
<MenuItem
|
||||
onClick={onEditExt}
|
||||
disabled={ !selectedExternal }
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EditOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
Edit Network
|
||||
</MenuItem> */}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={onDelExt}
|
||||
disabled={ !selectedExternal }
|
||||
|
@ -253,6 +260,7 @@ const Networks = (props) => {
|
|||
const [columnSortState, setColumnSortState] = React.useState({});
|
||||
|
||||
const [addExtOpen, setAddExtOpen] = React.useState(false);
|
||||
const [editExtOpen, setEditExtOpen] = React.useState(false);
|
||||
const [delExtOpen, setDelExtOpen] = React.useState(false);
|
||||
|
||||
const viewSetting = useSelector(state => selectViewSetting(state, 'extnetworks'));
|
||||
|
@ -486,6 +494,14 @@ const Networks = (props) => {
|
|||
block={selectedBlock ? selectedBlock : null}
|
||||
externals={externals}
|
||||
/>
|
||||
<EditExtNetwork
|
||||
open={editExtOpen}
|
||||
handleClose={() => setEditExtOpen(false)}
|
||||
space={selectedSpace ? selectedSpace.name : null}
|
||||
block={selectedBlock ? selectedBlock : null}
|
||||
externals={externals}
|
||||
selectedExternal={selectedExternal}
|
||||
/>
|
||||
<DeleteExtNetwork
|
||||
open={delExtOpen}
|
||||
handleClose={() => setDelExtOpen(false)}
|
||||
|
@ -493,7 +509,7 @@ const Networks = (props) => {
|
|||
block={selectedBlock ? selectedBlock.name : null}
|
||||
external={selectedExternal ? selectedExternal.name : null}
|
||||
/>
|
||||
<ExtNetworkContext.Provider value={{ selectedSpace, selectedBlock, selectedExternal, setAddExtOpen, setDelExtOpen, selectionModel, saving, sendResults, saveConfig, loadConfig, resetConfig }}>
|
||||
<ExtNetworkContext.Provider value={{ selectedSpace, selectedBlock, selectedExternal, setAddExtOpen, setEditExtOpen, setDelExtOpen, selectionModel, saving, sendResults, saveConfig, loadConfig, resetConfig }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%'}}>
|
||||
<Box sx={{ display: 'flex', height: '35px', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(224, 224, 224, 1)', borderBottom: 'none' }}>
|
||||
<Typography variant='button'>
|
||||
|
|
|
@ -0,0 +1,363 @@
|
|||
import * as React from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Tooltip,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Paper
|
||||
} from "@mui/material";
|
||||
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import {
|
||||
selectNetworks,
|
||||
updateBlockExternalAsync
|
||||
} from "../../../../ipam/ipamSlice";
|
||||
|
||||
import {
|
||||
isSubnetOf,
|
||||
isSubnetOverlap
|
||||
} from "../../../../tools/planner/utils/iputils";
|
||||
|
||||
import {
|
||||
EXTERNAL_NAME_REGEX,
|
||||
EXTERNAL_DESC_REGEX,
|
||||
CIDR_REGEX
|
||||
} from "../../../../../global/globals";
|
||||
|
||||
function DraggablePaper(props) {
|
||||
const nodeRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
handle="#draggable-dialog-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
bounds="parent"
|
||||
>
|
||||
<Paper {...props} ref={nodeRef}/>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EditExtNetwork(props) {
|
||||
const { open, handleClose, space, block, externals, selectedExternal } = props;
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [extName, setExtName] = React.useState({ value: "", error: false });
|
||||
const [extDesc, setExtDesc] = React.useState({ value: "", error: false });
|
||||
const [extCidr, setExtCidr] = React.useState({ value: "", error: false });
|
||||
|
||||
const [sending, setSending] = React.useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const networks = useSelector(selectNetworks);
|
||||
|
||||
function onCancel() {
|
||||
handleClose();
|
||||
|
||||
if(selectedExternal) {
|
||||
setExtName({ value: selectedExternal.name, error: false });
|
||||
setExtDesc({ value: selectedExternal.desc, error: false });
|
||||
setExtCidr({ value: selectedExternal.cidr, error: false });
|
||||
} else {
|
||||
setExtName({ value: "", error: false });
|
||||
setExtDesc({ value: "", error: false });
|
||||
setExtCidr({ value: "", error: false });
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
var body = [
|
||||
{
|
||||
op: "replace",
|
||||
path: "/name",
|
||||
value: extName.value
|
||||
},
|
||||
{
|
||||
op: "replace",
|
||||
path: "/desc",
|
||||
value: extDesc.value,
|
||||
},
|
||||
{
|
||||
op: "replace",
|
||||
path: "/cidr",
|
||||
value: extCidr.value
|
||||
}
|
||||
];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
setSending(true);
|
||||
await dispatch(updateBlockExternalAsync({ space: space, block: block.name, external: selectedExternal.name, body: body }));
|
||||
enqueueSnackbar("Successfully updated External Network", { variant: "success" });
|
||||
onCancel();
|
||||
} catch (e) {
|
||||
console.log("ERROR");
|
||||
console.log("------------------");
|
||||
console.log(e);
|
||||
console.log("------------------");
|
||||
enqueueSnackbar(e.message, { variant: "error" });
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function onNameChange(event) {
|
||||
const newName = event.target.value;
|
||||
|
||||
if(externals) {
|
||||
const regex = new RegExp(
|
||||
EXTERNAL_NAME_REGEX
|
||||
);
|
||||
|
||||
const nameError = newName ? !regex.test(newName) : false;
|
||||
const nameExists = externals?.reduce((acc, curr) => {
|
||||
curr['name'] !== selectedExternal['name'] && acc.push(curr['name'].toLowerCase());
|
||||
|
||||
return acc;
|
||||
}, []).includes(newName.toLowerCase());
|
||||
|
||||
setExtName({
|
||||
value: newName,
|
||||
error: (nameError || nameExists)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onDescChange(event) {
|
||||
const newDesc = event.target.value;
|
||||
|
||||
const regex = new RegExp(
|
||||
EXTERNAL_DESC_REGEX
|
||||
);
|
||||
|
||||
setExtDesc({
|
||||
value: newDesc,
|
||||
error: (newDesc ? !regex.test(newDesc) : false)
|
||||
});
|
||||
}
|
||||
|
||||
function onCidrChange(event) {
|
||||
const newCidr = event.target.value;
|
||||
|
||||
const regex = new RegExp(
|
||||
CIDR_REGEX
|
||||
);
|
||||
|
||||
const cidrError = newCidr ? !regex.test(newCidr) : false;
|
||||
|
||||
var blockNetworks= [];
|
||||
var extNetworks = [];
|
||||
|
||||
var cidrInBlock = false;
|
||||
var resvOverlap = true;
|
||||
var vnetOverlap = true;
|
||||
var extOverlap = true;
|
||||
|
||||
if(!cidrError && newCidr.length > 0) {
|
||||
cidrInBlock = isSubnetOf(newCidr, block.cidr);
|
||||
|
||||
const openResv = block?.resv.reduce((acc, curr) => {
|
||||
if(!curr['settledOn']) {
|
||||
acc.push(curr['cidr']);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(space && block && networks) {
|
||||
blockNetworks = networks?.reduce((acc, curr) => {
|
||||
if(curr['parent_space'] && curr['parent_block']) {
|
||||
if(curr['parent_space'] === space && curr['parent_block'].includes(block.name)) {
|
||||
acc = acc.concat(curr['prefixes']);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if(externals) {
|
||||
extNetworks = externals?.reduce((acc, curr) => {
|
||||
curr['name'] !== selectedExternal['name'] && acc.push(curr['cidr']);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
resvOverlap = isSubnetOverlap(newCidr, openResv);
|
||||
vnetOverlap = isSubnetOverlap(newCidr, blockNetworks);
|
||||
extOverlap = isSubnetOverlap(newCidr, extNetworks);
|
||||
}
|
||||
|
||||
setExtCidr({
|
||||
value: newCidr,
|
||||
error: (cidrError || !cidrInBlock || resvOverlap || vnetOverlap || extOverlap)
|
||||
});
|
||||
}
|
||||
|
||||
const unchanged = React.useMemo(() => {
|
||||
if(selectedExternal) {
|
||||
return (
|
||||
extName.value === selectedExternal.name &&
|
||||
extDesc.value === selectedExternal.desc &&
|
||||
extCidr.value === selectedExternal.cidr
|
||||
);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}, [selectedExternal, extName, extDesc, extCidr]);
|
||||
|
||||
const hasError = React.useMemo(() => {
|
||||
var emptyCheck = false;
|
||||
var errorCheck = false;
|
||||
|
||||
errorCheck = (extName.error || extDesc.error || extCidr.error);
|
||||
emptyCheck = (extName.value.length === 0 || extDesc.value.length === 0 || extCidr.value.length === 0);
|
||||
|
||||
return (errorCheck || emptyCheck);
|
||||
}, [extName, extDesc, extCidr]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedExternal) {
|
||||
setExtName({ value: selectedExternal.name, error: false });
|
||||
setExtDesc({ value: selectedExternal.desc, error: false });
|
||||
setExtCidr({ value: selectedExternal.cidr, error: false });
|
||||
} else {
|
||||
handleClose();
|
||||
|
||||
setExtName({ value: "", error: false });
|
||||
setExtDesc({ value: "", error: false });
|
||||
setExtCidr({ value: "", error: false });
|
||||
}
|
||||
}, [selectedExternal, handleClose]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
PaperComponent={DraggablePaper}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle style={{ cursor: 'move' }} id="draggable-dialog-title">
|
||||
Edit External Network
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" flexDirection="column" alignItems="center">
|
||||
<Tooltip
|
||||
arrow
|
||||
disableFocusListener
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
- Network name must be unique
|
||||
<br />- Max of 64 characters
|
||||
<br />- Can contain alphnumerics
|
||||
<br />- Can contain underscore, hypen and period
|
||||
<br />- Cannot start/end with underscore, hypen or period
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
autoFocus
|
||||
error={extName.error}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Name"
|
||||
type="name"
|
||||
variant="standard"
|
||||
value={extName.value}
|
||||
onChange={(event) => onNameChange(event)}
|
||||
inputProps={{ spellCheck: false }}
|
||||
sx={{ width: "80%" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
arrow
|
||||
disableFocusListener
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
- Max of 128 characters
|
||||
<br />- Can contain alphnumerics
|
||||
<br />- Can contain spaces
|
||||
<br />- Can contain underscore, hypen, slash and period
|
||||
<br />- Cannot start/end with underscore, hypen, slash or period
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
error={extDesc.error}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Description"
|
||||
type="description"
|
||||
variant="standard"
|
||||
value={extDesc.value}
|
||||
onChange={(event) => onDescChange(event)}
|
||||
inputProps={{ spellCheck: false }}
|
||||
sx={{ width: "80%" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
arrow
|
||||
disableFocusListener
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
- Must be in valid CIDR notation format
|
||||
<br /> - Example: 1.2.3.4/5
|
||||
<br />- Cannot overlap existing subnets
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
error={(extCidr.value.length > 0 && extCidr.error)}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="CIDR"
|
||||
type="cidr"
|
||||
variant="standard"
|
||||
value={extCidr.value}
|
||||
onChange={(event) => onCidrChange(event)}
|
||||
inputProps={{ spellCheck: false }}
|
||||
sx={{ width: "80%" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
disabled={sending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
onClick={onSubmit}
|
||||
loading={sending}
|
||||
disabled={hasError || unchanged}
|
||||
>
|
||||
Update
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -29,7 +29,7 @@ import {
|
|||
TaskAltOutlined,
|
||||
CancelOutlined,
|
||||
AddOutlined,
|
||||
// EditOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutline,
|
||||
EditNoteOutlined
|
||||
} from "@mui/icons-material";
|
||||
|
@ -40,6 +40,7 @@ import {
|
|||
} from "../../../ipam/ipamSlice";
|
||||
|
||||
import AddExtSubnet from "./utils/addSubnet";
|
||||
import EditExtSubnet from "./utils/editSubnet";
|
||||
import DeleteExtSubnet from "./utils/deleteSubnet";
|
||||
import ManageExtEndpoints from "./utils/manageEndpoints";
|
||||
|
||||
|
@ -59,6 +60,7 @@ function HeaderMenu(props) {
|
|||
selectedExternal,
|
||||
selectedSubnet,
|
||||
setAddExtSubOpen,
|
||||
setEditExtSubOpen,
|
||||
setDelExtSubOpen,
|
||||
setManExtEndOpen,
|
||||
saving,
|
||||
|
@ -83,6 +85,11 @@ function HeaderMenu(props) {
|
|||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
const onEditExtSub = () => {
|
||||
setEditExtSubOpen(true);
|
||||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
const onDelExtSub = () => {
|
||||
setDelExtSubOpen(true);
|
||||
setMenuOpen(false);
|
||||
|
@ -190,15 +197,15 @@ function HeaderMenu(props) {
|
|||
</ListItemIcon>
|
||||
Add Subnet
|
||||
</MenuItem>
|
||||
{/* <MenuItem
|
||||
onClick={() => console.log("EDIT!")}
|
||||
disabled={ true }
|
||||
<MenuItem
|
||||
onClick={onEditExtSub}
|
||||
disabled={ !selectedSubnet }
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EditOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
Edit Subnet
|
||||
</MenuItem> */}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={onDelExtSub}
|
||||
disabled={ !selectedSubnet }
|
||||
|
@ -270,6 +277,7 @@ const Subnets = (props) => {
|
|||
const [columnSortState, setColumnSortState] = React.useState({});
|
||||
|
||||
const [addExtSubOpen, setAddExtSubOpen] = React.useState(false);
|
||||
const [editExtSubOpen, setEditExtSubOpen] = React.useState(false);
|
||||
const [delExtSubOpen, setDelExtSubOpen] = React.useState(false);
|
||||
const [manExtEndOpen, setManExtEndOpen] = React.useState(false);
|
||||
|
||||
|
@ -505,6 +513,15 @@ const Subnets = (props) => {
|
|||
external={selectedExternal ? selectedExternal : null}
|
||||
subnets={subnets}
|
||||
/>
|
||||
<EditExtSubnet
|
||||
open={editExtSubOpen}
|
||||
handleClose={() => setEditExtSubOpen(false)}
|
||||
space={selectedSpace ? selectedSpace.name : null}
|
||||
block={selectedBlock ? selectedBlock.name : null}
|
||||
external={selectedExternal ? selectedExternal : null}
|
||||
subnets={subnets}
|
||||
selectedSubnet={selectedSubnet}
|
||||
/>
|
||||
<DeleteExtSubnet
|
||||
open={delExtSubOpen}
|
||||
handleClose={() => setDelExtSubOpen(false)}
|
||||
|
@ -521,7 +538,7 @@ const Subnets = (props) => {
|
|||
external={selectedExternal ? selectedExternal.name : null}
|
||||
subnet={selectedSubnet ? selectedSubnet : null}
|
||||
/>
|
||||
<ExtSubnetContext.Provider value={{ selectedExternal, selectedSubnet, setAddExtSubOpen, setDelExtSubOpen, setManExtEndOpen, saving, sendResults, saveConfig, loadConfig, resetConfig }}>
|
||||
<ExtSubnetContext.Provider value={{ selectedExternal, selectedSubnet, setAddExtSubOpen, setEditExtSubOpen, setDelExtSubOpen, setManExtEndOpen, saving, sendResults, saveConfig, loadConfig, resetConfig }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%'}}>
|
||||
<Box sx={{ display: 'flex', height: '35px', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(224, 224, 224, 1)', borderBottom: 'none' }}>
|
||||
<Typography variant='button'>
|
||||
|
|
|
@ -36,7 +36,6 @@ import {
|
|||
CIDR_REGEX,
|
||||
cidrMasks
|
||||
} from "../../../../../global/globals";
|
||||
import { Spellcheck } from "@mui/icons-material";
|
||||
|
||||
function DraggablePaper(props) {
|
||||
const nodeRef = React.useRef(null);
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
import * as React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Tooltip,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Paper
|
||||
} from "@mui/material";
|
||||
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import {
|
||||
updateBlockExtSubnetAsync
|
||||
} from "../../../../ipam/ipamSlice";
|
||||
|
||||
import {
|
||||
isSubnetOf,
|
||||
isSubnetOverlap
|
||||
} from "../../../../tools/planner/utils/iputils";
|
||||
|
||||
import {
|
||||
EXTSUBNET_NAME_REGEX,
|
||||
EXTSUBNET_DESC_REGEX,
|
||||
CIDR_REGEX
|
||||
} from "../../../../../global/globals";
|
||||
|
||||
function DraggablePaper(props) {
|
||||
const nodeRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
handle="#draggable-dialog-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
bounds="parent"
|
||||
>
|
||||
<Paper {...props} ref={nodeRef}/>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EditExtSubnet(props) {
|
||||
const { open, handleClose, space, block, external, subnets, selectedSubnet } = props;
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [subName, setSubName] = React.useState({ value: "", error: false });
|
||||
const [subDesc, setSubDesc] = React.useState({ value: "", error: false });
|
||||
const [subCidr, setSubCidr] = React.useState({ value: "", error: false });
|
||||
|
||||
const [sending, setSending] = React.useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
function onCancel() {
|
||||
handleClose();
|
||||
|
||||
if(selectedSubnet) {
|
||||
setSubName({ value: selectedSubnet.name, error: false });
|
||||
setSubDesc({ value: selectedSubnet.desc, error: false });
|
||||
setSubCidr({ value: selectedSubnet.cidr, error: false });
|
||||
} else {
|
||||
setSubName({ value: "", error: false });
|
||||
setSubDesc({ value: "", error: false });
|
||||
setSubCidr({ value: "", error: false });
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
var body = [
|
||||
{
|
||||
op: "replace",
|
||||
path: "/name",
|
||||
value: subName.value
|
||||
},
|
||||
{
|
||||
op: "replace",
|
||||
path: "/desc",
|
||||
value: subDesc.value
|
||||
},
|
||||
{
|
||||
op: "replace",
|
||||
path: "/cidr",
|
||||
value: subCidr.value
|
||||
}
|
||||
];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
setSending(true);
|
||||
await dispatch(updateBlockExtSubnetAsync({ space: space, block: block, external: external.name, subnet: selectedSubnet.name, body: body }));
|
||||
enqueueSnackbar("Successfully updated External Subnet", { variant: "success" });
|
||||
onCancel();
|
||||
} catch (e) {
|
||||
console.log("ERROR");
|
||||
console.log("------------------");
|
||||
console.log(e);
|
||||
console.log("------------------");
|
||||
enqueueSnackbar(e.message, { variant: "error" });
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function onNameChange(event) {
|
||||
const newName = event.target.value;
|
||||
|
||||
if(subnets) {
|
||||
const regex = new RegExp(
|
||||
EXTSUBNET_NAME_REGEX
|
||||
);
|
||||
|
||||
const nameError = newName ? !regex.test(newName) : false;
|
||||
const nameExists = subnets?.reduce((acc, curr) => {
|
||||
curr['name'] !== selectedSubnet['name'] && acc.push(curr['name'].toLowerCase());
|
||||
|
||||
return acc;
|
||||
}, []).includes(newName.toLowerCase());
|
||||
|
||||
setSubName({
|
||||
value: newName,
|
||||
error: (nameError || nameExists)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onDescChange(event) {
|
||||
const newDesc = event.target.value;
|
||||
|
||||
const regex = new RegExp(
|
||||
EXTSUBNET_DESC_REGEX
|
||||
);
|
||||
|
||||
setSubDesc({
|
||||
value: newDesc,
|
||||
error: (newDesc ? !regex.test(newDesc) : false)
|
||||
});
|
||||
}
|
||||
|
||||
function onCidrChange(event) {
|
||||
const newCidr = event.target.value;
|
||||
|
||||
const regex = new RegExp(
|
||||
CIDR_REGEX
|
||||
);
|
||||
|
||||
const cidrError = newCidr ? !regex.test(newCidr) : false;
|
||||
|
||||
var extSubnets = [];
|
||||
|
||||
var cidrInBlock = false;
|
||||
var subOverlap = true;
|
||||
|
||||
if(!cidrError && newCidr.length > 0) {
|
||||
cidrInBlock = isSubnetOf(newCidr, external.cidr);
|
||||
|
||||
if(subnets) {
|
||||
extSubnets = subnets?.reduce((acc, curr) => {
|
||||
curr['name'] !== selectedSubnet['name'] && acc.push(curr['cidr']);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
subOverlap = isSubnetOverlap(newCidr, extSubnets);
|
||||
}
|
||||
|
||||
setSubCidr({
|
||||
value: newCidr,
|
||||
error: (cidrError || !cidrInBlock || subOverlap)
|
||||
});
|
||||
}
|
||||
|
||||
const unchanged = React.useMemo(() => {
|
||||
if(selectedSubnet) {
|
||||
return (
|
||||
subName.value === selectedSubnet.name &&
|
||||
subDesc.value === selectedSubnet.desc &&
|
||||
subCidr.value === selectedSubnet.cidr
|
||||
);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}, [selectedSubnet, subName, subDesc, subCidr]);
|
||||
|
||||
const hasError = React.useMemo(() => {
|
||||
var emptyCheck = false;
|
||||
var errorCheck = false;
|
||||
|
||||
errorCheck = (subName.error || subDesc.error || subCidr.error);
|
||||
emptyCheck = (subName.value.length === 0 || subDesc.value.length === 0 || subCidr.value.length === 0);
|
||||
|
||||
return (errorCheck || emptyCheck);
|
||||
}, [subName, subDesc, subCidr]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedSubnet) {
|
||||
setSubName({ value: selectedSubnet.name, error: false });
|
||||
setSubDesc({ value: selectedSubnet.desc, error: false });
|
||||
setSubCidr({ value: selectedSubnet.cidr, error: false });
|
||||
} else {
|
||||
handleClose();
|
||||
|
||||
setSubName({ value: "", error: false });
|
||||
setSubDesc({ value: "", error: false });
|
||||
setSubCidr({ value: "", error: false });
|
||||
}
|
||||
}, [selectedSubnet, handleClose]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
PaperComponent={DraggablePaper}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle style={{ cursor: 'move' }} id="draggable-dialog-title">
|
||||
Edit External Subnet
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" flexDirection="column" alignItems="center">
|
||||
<Tooltip
|
||||
arrow
|
||||
disableFocusListener
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
- Subnet name must be unique
|
||||
<br />- Max of 64 characters
|
||||
<br />- Can contain alphnumerics
|
||||
<br />- Can contain underscore, hypen and period
|
||||
<br />- Cannot start/end with underscore, hypen or period
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
autoFocus
|
||||
error={subName.error}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Name"
|
||||
type="name"
|
||||
variant="standard"
|
||||
value={subName.value}
|
||||
onChange={(event) => onNameChange(event)}
|
||||
inputProps={{ spellCheck: false }}
|
||||
sx={{width: "80%" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
arrow
|
||||
disableFocusListener
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
- Max of 128 characters
|
||||
<br />- Can contain alphnumerics
|
||||
<br />- Can contain spaces
|
||||
<br />- Can contain underscore, hypen, slash and period
|
||||
<br />- Cannot start/end with underscore, hypen, slash or period
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
error={subDesc.error}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Description"
|
||||
type="description"
|
||||
variant="standard"
|
||||
value={subDesc.value}
|
||||
onChange={(event) => onDescChange(event)}
|
||||
inputProps={{ spellCheck: false }}
|
||||
sx={{ width: "80%" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
arrow
|
||||
disableFocusListener
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
- Must be in valid CIDR notation format
|
||||
<br /> - Example: 1.2.3.4/5
|
||||
<br />- Cannot overlap existing subnets
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
error={(subCidr.value.length > 0 && subCidr.error)}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="CIDR"
|
||||
type="cidr"
|
||||
variant="standard"
|
||||
value={subCidr.value}
|
||||
onChange={(event) => onCidrChange(event)}
|
||||
inputProps={{ spellCheck: false }}
|
||||
sx={{ width: "80%" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
disabled={sending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
onClick={onSubmit}
|
||||
loading={sending}
|
||||
disabled={hasError || unchanged}
|
||||
>
|
||||
Update
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
import { isEmpty, isEqual, pickBy, orderBy, cloneDeep } from "lodash";
|
||||
import { omit, isEmpty, isEqual, pickBy, orderBy, cloneDeep } from "lodash";
|
||||
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
|
@ -14,6 +14,8 @@ import Draggable from "react-draggable";
|
|||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import md5 from "md5";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -43,7 +45,9 @@ import {
|
|||
TaskAltOutlined,
|
||||
CancelOutlined,
|
||||
PlaylistAddOutlined,
|
||||
HighlightOff,
|
||||
PlaylistAddCheckOutlined,
|
||||
PlaylistRemoveOutlined,
|
||||
// HighlightOff,
|
||||
InfoOutlined
|
||||
} from "@mui/icons-material";
|
||||
|
||||
|
@ -56,7 +60,8 @@ import {
|
|||
} from "../../../../ipam/ipamSlice";
|
||||
|
||||
import {
|
||||
expandCIDR
|
||||
expandCIDR,
|
||||
getSubnetSize
|
||||
} from "../../../../tools/planner/utils/iputils";
|
||||
|
||||
import {
|
||||
|
@ -87,7 +92,7 @@ const gridStyle = {
|
|||
|
||||
function RenderDelete(props) {
|
||||
const { value } = props;
|
||||
const { endpoints, setAdded, deleted, setDeleted, selectionModel } = React.useContext(EndpointContext);
|
||||
const { setChanges, selectionModel } = React.useContext(EndpointContext);
|
||||
|
||||
const flexCenter = {
|
||||
display: "flex",
|
||||
|
@ -108,14 +113,18 @@ function RenderDelete(props) {
|
|||
disableTouchRipple
|
||||
disableRipple
|
||||
onClick={() => {
|
||||
if(endpoints.find(e => e.name === value.name) && !deleted.includes(value.name)) {
|
||||
setDeleted(prev => [...prev, value.name]);
|
||||
} else {
|
||||
setAdded(prev => prev.filter(e => e.name !== value.name));
|
||||
}
|
||||
var endpointDetails = cloneDeep(value);
|
||||
|
||||
endpointDetails['op'] = "delete";
|
||||
|
||||
setChanges(prev => [
|
||||
...prev,
|
||||
endpointDetails
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<HighlightOff />
|
||||
{/* <HighlightOff /> */}
|
||||
<PlaylistRemoveOutlined />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
@ -284,8 +293,7 @@ export default function ManageExtEndpoints(props) {
|
|||
const [sendResults, setSendResults] = React.useState(null);
|
||||
const [endpoints, setEndpoints] = React.useState(null);
|
||||
const [addressOptions, setAddressOptions] = React.useState([]);
|
||||
const [added, setAdded] = React.useState([]);
|
||||
const [deleted, setDeleted] = React.useState([]);
|
||||
const [changes, setChanges] = React.useState([]);
|
||||
const [gridData, setGridData] = React.useState(null);
|
||||
const [sending, setSending] = React.useState(false);
|
||||
const [selectionModel, setSelectionModel] = React.useState({});
|
||||
|
@ -331,6 +339,31 @@ export default function ManageExtEndpoints(props) {
|
|||
setSelectionModel(prevState => {
|
||||
if(!prevState.hasOwnProperty(id)) {
|
||||
newSelectionModel[id] = data;
|
||||
|
||||
setEndName({ value: data.name, error: false });
|
||||
setEndDesc({ value: data.desc, error: false });
|
||||
setEndAddrInput(data.ip);
|
||||
setEndAddr(data.ip);
|
||||
|
||||
const endpointAddresses = endpoints.map(e => {
|
||||
if (data.ip !== e.ip) {
|
||||
return e.ip;
|
||||
}
|
||||
});
|
||||
|
||||
const newAddressOptions = expandCIDR(subnet.cidr).slice(1,-1).filter(addr => !endpointAddresses.includes(addr));
|
||||
|
||||
setAddressOptions(newAddressOptions);
|
||||
} else {
|
||||
setEndName({ value: "", error: true });
|
||||
setEndDesc({ value: "", error: true });
|
||||
setEndAddrInput("");
|
||||
setEndAddr(null);
|
||||
|
||||
const endpointAddresses = endpoints.map(e => e.ip);
|
||||
const newAddressOptions = expandCIDR(subnet.cidr).slice(1,-1).filter(addr => !endpointAddresses.includes(addr));
|
||||
|
||||
setAddressOptions(["<auto>", ...newAddressOptions]);
|
||||
}
|
||||
|
||||
return newSelectionModel;
|
||||
|
@ -482,14 +515,43 @@ export default function ManageExtEndpoints(props) {
|
|||
|
||||
function onAddExternal() {
|
||||
if(!hasError) {
|
||||
setAdded(prev => [
|
||||
...prev,
|
||||
{
|
||||
name: endName.value,
|
||||
desc: endDesc.value,
|
||||
ip: endAddr
|
||||
var endpointDetails = {
|
||||
name: endName.value,
|
||||
desc: endDesc.value,
|
||||
ip: endAddr
|
||||
};
|
||||
|
||||
endpointDetails['id'] = md5(JSON.stringify(endpointDetails));
|
||||
|
||||
if (Object.keys(selectionModel).length !== 0) {
|
||||
const updates = {
|
||||
op: "update",
|
||||
old: Object.values(selectionModel)[0],
|
||||
new: endpointDetails
|
||||
}
|
||||
]);
|
||||
|
||||
setChanges(prev => [
|
||||
...prev,
|
||||
updates
|
||||
]);
|
||||
} else {
|
||||
const numEndpoints = endpoints.length;
|
||||
const numAdditions = changes.filter(change => change.op === "add").length;
|
||||
const numDeletions = changes.filter(change => change.op === "delete").length;
|
||||
const subnetSize = getSubnetSize(subnet.cidr) - 2;
|
||||
|
||||
if (((numEndpoints + numAdditions) - numDeletions) >= subnetSize) {
|
||||
enqueueSnackbar(`Number of endpoints cannot exceed subnet size of ${subnetSize}`, { variant: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
endpointDetails['op'] = "add";
|
||||
|
||||
setChanges(prev => [
|
||||
...prev,
|
||||
endpointDetails
|
||||
]);
|
||||
}
|
||||
|
||||
setEndName({ value: "", error: true });
|
||||
setEndDesc({ value: "", error: true });
|
||||
|
@ -533,15 +595,15 @@ export default function ManageExtEndpoints(props) {
|
|||
|
||||
const onCancel = React.useCallback(() => {
|
||||
if (open) {
|
||||
setAdded([]);
|
||||
setDeleted([]);
|
||||
handleClose();
|
||||
|
||||
setSelectionModel({});
|
||||
setChanges([]);
|
||||
|
||||
setEndName({ value: "", error: true });
|
||||
setEndDesc({ value: "", error: true });
|
||||
setEndAddrInput("");
|
||||
setEndAddr(null);
|
||||
|
||||
handleClose();
|
||||
}
|
||||
}, [open, handleClose]);
|
||||
|
||||
|
@ -561,7 +623,17 @@ export default function ManageExtEndpoints(props) {
|
|||
);
|
||||
|
||||
const nameError = newName ? !regex.test(newName) : false;
|
||||
const nameExists = endpoints.map(e => e.name.toLowerCase()).includes(newName.toLowerCase());
|
||||
const nameExists = endpoints?.reduce((acc, curr) => {
|
||||
if(Object.keys(selectionModel).length !== 0) {
|
||||
if (curr['name'].toLowerCase() !== Object.values(selectionModel)[0].name.toLowerCase()) {
|
||||
acc.push(curr['name'].toLowerCase());
|
||||
}
|
||||
} else {
|
||||
acc.push(curr['name'].toLowerCase());
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).includes(newName.toLowerCase());
|
||||
|
||||
setEndName({
|
||||
value: newName,
|
||||
|
@ -594,17 +666,41 @@ export default function ManageExtEndpoints(props) {
|
|||
if(subnet) {
|
||||
var newEndpoints = cloneDeep(subnet['endpoints']);
|
||||
|
||||
newEndpoints = newEndpoints.filter(e => !deleted.includes(e.name));
|
||||
newEndpoints = newEndpoints.concat(added);
|
||||
|
||||
const newData = newEndpoints.reduce((acc, curr) => {
|
||||
curr['id'] = `${subnet}@${curr.name}}`
|
||||
var newData = newEndpoints.reduce((acc, curr) => {
|
||||
// curr['id'] = `${subnet}@${curr.name}}`;
|
||||
curr['id'] = md5(JSON.stringify(curr));
|
||||
|
||||
acc.push(curr);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
changes.forEach(change => {
|
||||
switch(change.op) {
|
||||
case "add": {
|
||||
newData.push(omit(change, 'op'));
|
||||
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
const index = newData.findIndex(e => e.name === change.old.name);
|
||||
|
||||
if (index !== -1) {
|
||||
newData[index] = change.new;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "delete": {
|
||||
newData = newData.filter(e => e.name !== change.name);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const endpointAddresses = newData.map(e => e.ip);
|
||||
const newAddressOptions = expandCIDR(subnet.cidr).slice(1,-1).filter(addr => !endpointAddresses.includes(addr));
|
||||
|
||||
|
@ -613,10 +709,10 @@ export default function ManageExtEndpoints(props) {
|
|||
} else {
|
||||
onCancel();
|
||||
}
|
||||
}, [subnet, added, deleted, onCancel]);
|
||||
}, [subnet, changes, onCancel]);
|
||||
|
||||
return (
|
||||
<EndpointContext.Provider value={{ endpoints, setAdded, deleted, setDeleted, selectionModel, saving, sendResults, saveConfig, loadConfig, resetConfig }}>
|
||||
<EndpointContext.Provider value={{ endpoints, setChanges, selectionModel, saving, sendResults, saveConfig, loadConfig, resetConfig }}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
|
@ -674,7 +770,11 @@ export default function ManageExtEndpoints(props) {
|
|||
color: theme.palette.mode === 'dark' ? '#9ba7b4' : '#555e68'
|
||||
}}
|
||||
>
|
||||
Add New Endpoint
|
||||
{
|
||||
Object.keys(selectionModel).length !== 0 ?
|
||||
"Edit Existing Endpoint" :
|
||||
"Add New Endpoint"
|
||||
}
|
||||
</span>
|
||||
</Box>
|
||||
<Box
|
||||
|
@ -884,17 +984,31 @@ export default function ManageExtEndpoints(props) {
|
|||
disabled={(hasError || sending || refreshing)}
|
||||
onClick={onAddExternal}
|
||||
>
|
||||
<PlaylistAddOutlined
|
||||
style={
|
||||
theme.palette.mode === 'dark'
|
||||
? (hasError || sending || refreshing)
|
||||
? { color: "lightgrey", opacity: 0.25 }
|
||||
: { color: "forestgreen", opacity: 1 }
|
||||
: (hasError || sending || refreshing)
|
||||
? { color: "black", opacity: 0.25 }
|
||||
: { color: "limegreen", opacity: 1 }
|
||||
}
|
||||
/>
|
||||
{
|
||||
Object.keys(selectionModel).length !== 0 ?
|
||||
<PlaylistAddCheckOutlined
|
||||
style={
|
||||
theme.palette.mode === 'dark'
|
||||
? (hasError || sending || refreshing)
|
||||
? { color: "lightgrey", opacity: 0.25 }
|
||||
: { color: "forestgreen", opacity: 1 }
|
||||
: (hasError || sending || refreshing)
|
||||
? { color: "black", opacity: 0.25 }
|
||||
: { color: "limegreen", opacity: 1 }
|
||||
}
|
||||
/> :
|
||||
<PlaylistAddOutlined
|
||||
style={
|
||||
theme.palette.mode === 'dark'
|
||||
? (hasError || sending || refreshing)
|
||||
? { color: "lightgrey", opacity: 0.25 }
|
||||
: { color: "forestgreen", opacity: 1 }
|
||||
: (hasError || sending || refreshing)
|
||||
? { color: "black", opacity: 0.25 }
|
||||
: { color: "limegreen", opacity: 1 }
|
||||
}
|
||||
/>
|
||||
}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
|
|
@ -149,6 +149,12 @@ export function createBlockExternal(space, block, body) {
|
|||
return api.post(url, body);
|
||||
}
|
||||
|
||||
export function updateBlockExternal(space, block, external, body) {
|
||||
const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}`);
|
||||
|
||||
return api.patch(url, body);
|
||||
}
|
||||
|
||||
export function deleteBlockExternal(space, block, external, force) {
|
||||
const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}`);
|
||||
var urlParams = url.searchParams;
|
||||
|
@ -164,6 +170,12 @@ export function createBlockExtSubnet(space, block, external, body) {
|
|||
return api.post(url, body);
|
||||
}
|
||||
|
||||
export function updateBlockExtSubnet(space, block, external, subnet, body) {
|
||||
const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}/subnets/${subnet}`);
|
||||
|
||||
return api.patch(url, body);
|
||||
}
|
||||
|
||||
export function deleteBlockExtSubnet(space, block, external, subnet, force) {
|
||||
const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}/subnets/${subnet}`);
|
||||
var urlParams = url.searchParams;
|
||||
|
|
|
@ -13,8 +13,10 @@ import {
|
|||
updateBlock,
|
||||
deleteBlock,
|
||||
createBlockExternal,
|
||||
updateBlockExternal,
|
||||
deleteBlockExternal,
|
||||
createBlockExtSubnet,
|
||||
updateBlockExtSubnet,
|
||||
deleteBlockExtSubnet,
|
||||
replaceBlockExtSubnetEndpoints,
|
||||
createBlockResv,
|
||||
|
@ -178,6 +180,19 @@ export const createBlockExternalAsync = createAsyncThunk(
|
|||
}
|
||||
);
|
||||
|
||||
export const updateBlockExternalAsync = createAsyncThunk(
|
||||
'ipam/updateBlockExternal',
|
||||
async (args, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await updateBlockExternal(args.space, args.block, args.external, args.body);
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
return rejectWithValue(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteBlockExternalAsync = createAsyncThunk(
|
||||
'ipam/deleteBlockExternal',
|
||||
async (args, { rejectWithValue }) => {
|
||||
|
@ -204,6 +219,19 @@ export const createBlockExtSubnetAsync = createAsyncThunk(
|
|||
}
|
||||
);
|
||||
|
||||
export const updateBlockExtSubnetAsync = createAsyncThunk(
|
||||
'ipam/updateBlockExtSubnet',
|
||||
async (args, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await updateBlockExtSubnet(args.space, args.block, args.external, args.subnet, args.body);
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
return rejectWithValue(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteBlockExtSubnetAsync = createAsyncThunk(
|
||||
'ipam/deleteBlockExtSubnet',
|
||||
async (args, { rejectWithValue }) => {
|
||||
|
@ -565,6 +593,31 @@ export const ipamSlice = createSlice({
|
|||
// SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
|
||||
throw action.payload;
|
||||
})
|
||||
.addCase(updateBlockExternalAsync.fulfilled, (state, action) => {
|
||||
const spaceName = action.meta.arg.space;
|
||||
const blockName = action.meta.arg.block;
|
||||
const externalName = action.meta.arg.external;
|
||||
const updatedExternal = action.payload;
|
||||
const spaceIndex = state.spaces.findIndex((x) => x.name === spaceName);
|
||||
|
||||
if (spaceIndex > -1) {
|
||||
const blockIndex = state.spaces[spaceIndex].blocks.findIndex((x) => x.name === blockName);
|
||||
|
||||
if(blockIndex > -1) {
|
||||
const externalIndex = state.spaces[spaceIndex].blocks[blockIndex].externals.findIndex((x) => x.name === externalName);
|
||||
|
||||
if(externalIndex > -1) {
|
||||
state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex] = merge(state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex], updatedExternal);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(updateBlockExternalAsync.rejected, (state, action) => {
|
||||
console.log("updateBlockExternalAsync Rejected");
|
||||
console.log(action);
|
||||
// SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
|
||||
throw action.payload;
|
||||
})
|
||||
.addCase(deleteBlockExternalAsync.fulfilled, (state, action) => {
|
||||
const spaceName = action.meta.arg.space;
|
||||
const blockName = action.meta.arg.block;
|
||||
|
@ -604,6 +657,36 @@ export const ipamSlice = createSlice({
|
|||
// SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
|
||||
throw action.payload;
|
||||
})
|
||||
.addCase(updateBlockExtSubnetAsync.fulfilled, (state, action) => {
|
||||
const spaceName = action.meta.arg.space;
|
||||
const blockName = action.meta.arg.block;
|
||||
const externalName = action.meta.arg.external;
|
||||
const subnetName = action.meta.arg.subnet;
|
||||
const updatedSubnet = action.payload;
|
||||
const spaceIndex = state.spaces.findIndex((x) => x.name === spaceName);
|
||||
|
||||
if (spaceIndex > -1) {
|
||||
const blockIndex = state.spaces[spaceIndex].blocks.findIndex((x) => x.name === blockName);
|
||||
|
||||
if(blockIndex > -1) {
|
||||
const externalIndex = state.spaces[spaceIndex].blocks[blockIndex].externals.findIndex((x) => x.name === externalName);
|
||||
|
||||
if(externalIndex > -1) {
|
||||
const subnetIndex = state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex].subnets.findIndex((x) => x.name === subnetName);
|
||||
|
||||
if(subnetIndex > -1) {
|
||||
state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex].subnets[subnetIndex] = merge(state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex].subnets[subnetIndex], updatedSubnet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(updateBlockExtSubnetAsync.rejected, (state, action) => {
|
||||
console.log("updateBlockExtSubnetAsync Rejected");
|
||||
console.log(action);
|
||||
// SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
|
||||
throw action.payload;
|
||||
})
|
||||
.addCase(deleteBlockExtSubnetAsync.fulfilled, (state, action) => {
|
||||
const spaceName = action.meta.arg.space;
|
||||
const blockName = action.meta.arg.block;
|
||||
|
|
|
@ -23,6 +23,13 @@ function getIpRangeForSubnet(cidr) {
|
|||
return results;
|
||||
}
|
||||
|
||||
export function getSubnetSize(cidr) {
|
||||
var mask = parseInt(cidr.split('/')[1], 10);
|
||||
var size = Math.pow(2, (32 - mask));
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export function isSubnetOverlap(subnetCIDR, existingSubnetCIDR) {
|
||||
var ipRangeforCurrent = getIpRangeForSubnet(subnetCIDR);
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче