mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(sdn-controller): traffic rules ordering (#10525)
This commit is contained in:
@@ -12,12 +12,20 @@ const BASE = '/rest/v0/plugins/sdn-controller'
|
||||
// Note : this test assumes that the config has the useDirectChannel option set to false
|
||||
// the old backend is being replaced and doesn't have support for network rules deletion, and is generally unstable.
|
||||
|
||||
// Priorities are unique on a network: the network rule tests delete theirs before the VIF ones add it
|
||||
const VALID_RULE = {
|
||||
allow: true,
|
||||
direction: 'from',
|
||||
ipRange: '10.0.0.0/8',
|
||||
protocol: 'TCP',
|
||||
port: 8080,
|
||||
priority: 31000,
|
||||
}
|
||||
|
||||
// the rules of a network or a VIF, stored as JSON strings inside their `other_config`
|
||||
const parseRules = object => {
|
||||
const raw = object.other_config?.['xo:sdn-controller:of-rules']
|
||||
return raw !== undefined ? JSON.parse(raw).map(JSON.parse) : []
|
||||
}
|
||||
|
||||
describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID }, () => {
|
||||
@@ -63,6 +71,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
try {
|
||||
await dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/add_traffic_rule?sync=true`, SENTINEL_RULE)
|
||||
await dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: SENTINEL_RULE.allow,
|
||||
direction: SENTINEL_RULE.direction,
|
||||
ipRange: SENTINEL_RULE.ipRange,
|
||||
protocol: SENTINEL_RULE.protocol,
|
||||
@@ -80,7 +89,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
const network = await dispatchClient.restApiClient.get(`/rest/v0/networks/${networkId}?fields=*`)
|
||||
const ofMethod =
|
||||
network.$pool !== undefined
|
||||
? (await dispatchClient.restApiClient.get(`/rest/v0/pools/${network.$pool}?fields=*`)).other_config?.[
|
||||
? (await dispatchClient.restApiClient.get(`/rest/v0/pools/${network.$pool}?fields=*`)).otherConfig?.[
|
||||
'xo:sdn-controller:of-method'
|
||||
]
|
||||
: undefined
|
||||
@@ -88,8 +97,8 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
if (ofMethod === undefined || ofMethod !== 'xapi-plugin') {
|
||||
skipReason =
|
||||
`SDN controller pool uses the '${ofMethod}' backend; this suite requires the ` +
|
||||
`'xapi-plugin' backend (set the useDirectChannel option to false in config.toml). ` +
|
||||
log.warn('SDN controller pre-check failed', { skipReason })
|
||||
`'xapi-plugin' backend (set the useDirectChannel option to false in config.toml).`
|
||||
log.warn('SDN controller pre-check failed', { skipReason })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -157,6 +166,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should reject delete when direction is missing', () =>
|
||||
rejectsWithHttpError(`${BASE}/networks/${networkId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
ipRange: '10.0.0.0/8',
|
||||
protocol: 'TCP',
|
||||
})
|
||||
@@ -164,6 +174,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should reject delete when ipRange is missing', () =>
|
||||
rejectsWithHttpError(`${BASE}/networks/${networkId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
direction: 'from',
|
||||
protocol: 'TCP',
|
||||
})
|
||||
@@ -171,6 +182,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should reject delete when protocol is missing', () =>
|
||||
rejectsWithHttpError(`${BASE}/networks/${networkId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
direction: 'from',
|
||||
ipRange: '10.0.0.0/8',
|
||||
})
|
||||
@@ -181,9 +193,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
describe('Network rules – CRUD', () => {
|
||||
const getNetworkRules = async () => {
|
||||
const network = await dispatchClient.restApiClient.get(`/rest/v0/networks/${networkId}?fields=*`)
|
||||
const raw = network.other_config?.['xo:sdn-controller:of-rules']
|
||||
return raw !== undefined ? JSON.parse(raw).map(JSON.parse) : []
|
||||
return parseRules(await dispatchClient.restApiClient.get(`/rest/v0/networks/${networkId}?fields=*`))
|
||||
}
|
||||
|
||||
const ruleKey = r => `${r.direction}:${r.ipRange}:${r.protocol}:${r.port}`
|
||||
@@ -199,26 +209,27 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
assert.strictEqual(found.allow, true)
|
||||
})
|
||||
|
||||
itSdn(
|
||||
'should overwrite the rule when added again with allow=false (no duplication, cookie preserved)',
|
||||
async () => {
|
||||
await dispatchClient.restApiClient.post(`${BASE}/networks/${networkId}/actions/add_traffic_rule?sync=true`, {
|
||||
...VALID_RULE,
|
||||
allow: false,
|
||||
})
|
||||
const rules = await getNetworkRules()
|
||||
const matching = rules.filter(r => ruleKey(r) === ruleKey(VALID_RULE))
|
||||
assert.strictEqual(matching.length, 1, 'Rule must not be duplicated')
|
||||
assert.strictEqual(matching[0].allow, false, 'Rule allow field must be updated')
|
||||
}
|
||||
)
|
||||
itSdn('should reject the rule added again with its priority, without duplicating it', async () => {
|
||||
await assert.rejects(
|
||||
dispatchClient.restApiClient.post(
|
||||
`${BASE}/networks/${networkId}/actions/add_traffic_rule?sync=true`,
|
||||
VALID_RULE
|
||||
),
|
||||
{ message: /^HTTP 409:/ }
|
||||
)
|
||||
const rules = await getNetworkRules()
|
||||
const matching = rules.filter(r => ruleKey(r) === ruleKey(VALID_RULE))
|
||||
assert.strictEqual(matching.length, 1, 'Rule must not be duplicated')
|
||||
})
|
||||
|
||||
itSdn('should delete the rule', async () => {
|
||||
await dispatchClient.restApiClient.post(`${BASE}/networks/${networkId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: VALID_RULE.allow,
|
||||
direction: VALID_RULE.direction,
|
||||
ipRange: VALID_RULE.ipRange,
|
||||
protocol: VALID_RULE.protocol,
|
||||
port: VALID_RULE.port,
|
||||
priority: VALID_RULE.priority,
|
||||
})
|
||||
const rules = await getNetworkRules()
|
||||
const found = rules.find(r => ruleKey(r) === ruleKey(VALID_RULE))
|
||||
@@ -227,13 +238,17 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
assert.strictEqual(found, undefined, 'Rule should be absent after delete')
|
||||
})
|
||||
|
||||
itSdn('should not error when deleting a non-existent rule', () =>
|
||||
dispatchClient.restApiClient.post(`${BASE}/networks/${networkId}/actions/delete_traffic_rule?sync=true`, {
|
||||
direction: 'to',
|
||||
ipRange: '192.168.99.0/24',
|
||||
protocol: 'UDP',
|
||||
port: 9999,
|
||||
})
|
||||
itSdn('should return 404 when deleting a non-existent rule', () =>
|
||||
assert.rejects(
|
||||
dispatchClient.restApiClient.post(`${BASE}/networks/${networkId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
direction: 'to',
|
||||
ipRange: '192.168.99.0/24',
|
||||
protocol: 'UDP',
|
||||
port: 9999,
|
||||
}),
|
||||
{ message: /^HTTP 404:/ }
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -297,6 +312,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should reject delete when direction is missing', () =>
|
||||
rejectsWithHttpError(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
ipRange: '10.0.0.0/8',
|
||||
protocol: 'TCP',
|
||||
})
|
||||
@@ -304,6 +320,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should reject delete when ipRange is missing', () =>
|
||||
rejectsWithHttpError(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
direction: 'from',
|
||||
protocol: 'TCP',
|
||||
})
|
||||
@@ -311,6 +328,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should reject delete when protocol is missing', () =>
|
||||
rejectsWithHttpError(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
direction: 'from',
|
||||
ipRange: '10.0.0.0/8',
|
||||
})
|
||||
@@ -321,9 +339,7 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
describe('VIF rules – CRUD', () => {
|
||||
const getVifRules = async () => {
|
||||
const vif = await dispatchClient.restApiClient.get(`/rest/v0/vifs/${vifId}?fields=*`)
|
||||
const raw = vif.other_config?.['xo:sdn-controller:of-rules']
|
||||
return raw !== undefined ? JSON.parse(raw).map(JSON.parse) : []
|
||||
return parseRules(await dispatchClient.restApiClient.get(`/rest/v0/vifs/${vifId}?fields=*`))
|
||||
}
|
||||
|
||||
const ruleKey = r => `${r.direction}:${r.ipRange}:${r.protocol}:${r.port}`
|
||||
@@ -336,8 +352,11 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
assert.strictEqual(found.allow, true)
|
||||
})
|
||||
|
||||
itSdn('should not duplicate the rule when added again', async () => {
|
||||
await dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/add_traffic_rule?sync=true`, VALID_RULE)
|
||||
itSdn('should reject the rule added again with its priority, without duplicating it', async () => {
|
||||
await assert.rejects(
|
||||
dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/add_traffic_rule?sync=true`, VALID_RULE),
|
||||
{ message: /^HTTP 409:/ }
|
||||
)
|
||||
const rules = await getVifRules()
|
||||
const matching = rules.filter(r => ruleKey(r) === ruleKey(VALID_RULE))
|
||||
assert.strictEqual(matching.length, 1, 'Rule must not be duplicated')
|
||||
@@ -345,10 +364,12 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
|
||||
itSdn('should delete the rule', async () => {
|
||||
await dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: VALID_RULE.allow,
|
||||
direction: VALID_RULE.direction,
|
||||
ipRange: VALID_RULE.ipRange,
|
||||
protocol: VALID_RULE.protocol,
|
||||
port: VALID_RULE.port,
|
||||
priority: VALID_RULE.priority,
|
||||
})
|
||||
const rules = await getVifRules()
|
||||
const found = rules.find(r => ruleKey(r) === ruleKey(VALID_RULE))
|
||||
@@ -356,13 +377,140 @@ describe('SDN Controller REST API', { skip: !process.env.SDN_CONTROLLER_VM_ID },
|
||||
assert.strictEqual(found, undefined, 'Rule should be absent after delete')
|
||||
})
|
||||
|
||||
itSdn('should not error when deleting a non-existent rule', () =>
|
||||
dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
direction: 'to',
|
||||
ipRange: '192.168.99.0/24',
|
||||
protocol: 'UDP',
|
||||
port: 9999,
|
||||
})
|
||||
itSdn('should return 404 when deleting a non-existent rule', () =>
|
||||
assert.rejects(
|
||||
dispatchClient.restApiClient.post(`${BASE}/vifs/${vifId}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow: true,
|
||||
direction: 'to',
|
||||
ipRange: '192.168.99.0/24',
|
||||
protocol: 'UDP',
|
||||
port: 9999,
|
||||
}),
|
||||
{ message: /^HTTP 404:/ }
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The client gives the OpenFlow priority itself: XO stores it and passes it to the XAPI plugin
|
||||
describe('Traffic rule priorities', () => {
|
||||
// a range VALID_RULE does not use: the cleanup deletes every network rule in it
|
||||
const TEST_IP_RANGE = '198.51.100.0/24'
|
||||
const NET_RULE = { allow: true, direction: 'to', ipRange: TEST_IP_RANGE, protocol: 'TCP', port: 443 }
|
||||
const VIF_RULE = { allow: true, direction: 'from', ipRange: TEST_IP_RANGE, protocol: 'TCP', port: 22 }
|
||||
|
||||
const ruleKey = r => `${r.direction}:${r.ipRange}:${r.protocol}:${r.port}`
|
||||
|
||||
const addRule = (collection, id, rule) =>
|
||||
dispatchClient.restApiClient.post(`${BASE}/${collection}/${id}/actions/add_traffic_rule?sync=true`, rule)
|
||||
const updateRule = (collection, id, oldRule, newRule) =>
|
||||
dispatchClient.restApiClient.post(`${BASE}/${collection}/${id}/actions/update_traffic_rule?sync=true`, {
|
||||
oldRule,
|
||||
newRule,
|
||||
})
|
||||
const deleteRule = (collection, id, { allow, direction, ipRange, protocol, port, priority }) =>
|
||||
dispatchClient.restApiClient.post(`${BASE}/${collection}/${id}/actions/delete_traffic_rule?sync=true`, {
|
||||
allow,
|
||||
direction,
|
||||
ipRange,
|
||||
protocol,
|
||||
port,
|
||||
priority,
|
||||
})
|
||||
|
||||
// `collection` is `networks` or `vifs`, in the core REST API as in the plugin's
|
||||
const rulesOf = async (collection, id) =>
|
||||
parseRules(await dispatchClient.restApiClient.get(`/rest/v0/${collection}/${id}?fields=*`))
|
||||
const findRule = (rules, rule) => rules.find(r => ruleKey(r) === ruleKey(rule))
|
||||
|
||||
const rejectsWithStatus = (promise, status) => assert.rejects(promise, { message: new RegExp(`^HTTP ${status}:`) })
|
||||
|
||||
// VIF rules go with the VMs, at teardown
|
||||
after(async () => {
|
||||
if (skipReason !== undefined) {
|
||||
return
|
||||
}
|
||||
for (const rule of await rulesOf('networks', networkId)) {
|
||||
if (rule.ipRange === TEST_IP_RANGE) {
|
||||
await deleteRule('networks', networkId, rule)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
itSdn('should store the priority of a network rule', async () => {
|
||||
await addRule('networks', networkId, { ...NET_RULE, priority: 40000 })
|
||||
const { cookie, ...rule } = findRule(await rulesOf('networks', networkId), NET_RULE)
|
||||
assert.deepStrictEqual(rule, { ...NET_RULE, priority: 40000 })
|
||||
})
|
||||
|
||||
itSdn('should store the priority of a VIF rule', async () => {
|
||||
await addRule('vifs', vifId, { ...VIF_RULE, priority: 50000 })
|
||||
assert.deepStrictEqual(findRule(await rulesOf('vifs', vifId), VIF_RULE), { ...VIF_RULE, priority: 50000 })
|
||||
})
|
||||
|
||||
itSdn('should change the priority of a rule', async () => {
|
||||
await updateRule('networks', networkId, { ...NET_RULE, priority: 40000 }, { priority: 45000 })
|
||||
assert.strictEqual(findRule(await rulesOf('networks', networkId), NET_RULE)?.priority, 45000)
|
||||
})
|
||||
|
||||
itSdn('should keep the priority when an update leaves it out', async () => {
|
||||
await updateRule('networks', networkId, { ...NET_RULE, priority: 45000 }, { allow: false })
|
||||
const rule = findRule(await rulesOf('networks', networkId), NET_RULE)
|
||||
assert.strictEqual(rule?.allow, false)
|
||||
assert.strictEqual(rule.priority, 45000)
|
||||
})
|
||||
|
||||
// XO 5 and XO 6 send none yet: such a rule has the OpenFlow default, as the rules from before priorities
|
||||
itSdn('should accept a rule without a priority, and refuse an update removing one', async () => {
|
||||
const rule = { ...NET_RULE, port: 1444 }
|
||||
await addRule('networks', networkId, rule)
|
||||
const { cookie, ...stored } = findRule(await rulesOf('networks', networkId), rule)
|
||||
assert.deepStrictEqual(stored, rule)
|
||||
await updateRule('networks', networkId, rule, { priority: 46000 })
|
||||
assert.strictEqual(findRule(await rulesOf('networks', networkId), rule)?.priority, 46000)
|
||||
await rejectsWithStatus(updateRule('networks', networkId, { ...rule, priority: 46000 }, { priority: null }), 422)
|
||||
})
|
||||
|
||||
// network and VIF rules alike
|
||||
itSdn('should reject a priority another rule of the network has', async () => {
|
||||
await rejectsWithStatus(addRule('networks', networkId, { ...NET_RULE, port: 2443, priority: 50000 }), 409)
|
||||
await rejectsWithStatus(addRule('vifs', vifId, { ...VIF_RULE, port: 2222, priority: 45000 }), 409)
|
||||
await rejectsWithStatus(
|
||||
updateRule('networks', networkId, { ...NET_RULE, allow: false, priority: 45000 }, { priority: 50000 }),
|
||||
409
|
||||
)
|
||||
})
|
||||
|
||||
itSdn('should reject a priority that is not an integer from 0 to 65535', async () => {
|
||||
const rule = { ...NET_RULE, port: 1443 }
|
||||
for (const priority of [-1, 1.5, 65536, '1']) {
|
||||
await rejectsWithStatus(addRule('networks', networkId, { ...rule, priority }), 422)
|
||||
}
|
||||
assert.strictEqual(findRule(await rulesOf('networks', networkId), rule), undefined)
|
||||
await rejectsWithStatus(
|
||||
updateRule('networks', networkId, { ...NET_RULE, allow: false, priority: 45000 }, { priority: 65536 }),
|
||||
422
|
||||
)
|
||||
})
|
||||
|
||||
// xo-cli calls the JSON-RPC methods. 0, a valid priority, is falsy
|
||||
itSdn('should take a priority through the JSON-RPC API too', async () => {
|
||||
const { allow, ...match } = { ...NET_RULE, port: 3443 }
|
||||
await assert.rejects(
|
||||
dispatchClient.xoClient.call('sdnController.addNetworkRule', { networkId, allow, ...match, priority: 65536 })
|
||||
)
|
||||
await dispatchClient.xoClient.call('sdnController.addNetworkRule', { networkId, allow, ...match, priority: 0 })
|
||||
assert.strictEqual(findRule(await rulesOf('networks', networkId), match)?.priority, 0)
|
||||
|
||||
await dispatchClient.xoClient.call('sdnController.deleteNetworkRule', { networkId, ...match, priority: 0 })
|
||||
assert.strictEqual(findRule(await rulesOf('networks', networkId), match), undefined)
|
||||
})
|
||||
|
||||
itSdn('should delete a rule that has a priority, only when given it', async () => {
|
||||
await rejectsWithStatus(deleteRule('vifs', vifId, VIF_RULE), 404)
|
||||
await deleteRule('vifs', vifId, { ...VIF_RULE, priority: 50000 })
|
||||
assert.strictEqual(findRule(await rulesOf('vifs', vifId), VIF_RULE), undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- [i18n] Update Chinese (Simplified Han script), Czech, Danish, Dutch, German, Italian, Portuguese, Portuguese (Brasil), Russian, Slovak, Spanish, Swedish and Ukrainian translations (PR [#10464](https://github.com/vatesfr/xen-orchestra/pull/10464))
|
||||
- [i18n] Update Arabic, Danish, Finnish, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese (Brasil), Russian and Ukrainian translations (PR [#10641](https://github.com/vatesfr/xen-orchestra/pull/10641))
|
||||
- [CLI] Add new command to `xo-server-db` cli: `disable-remote <id>`, disables a remote while xo-server is stopped (PR [#10587](https://github.com/vatesfr/xen-orchestra/pull/10587/))
|
||||
- [REST API/SDN Controller] With the XAPI plugin, traffic rules take an optional OpenFlow `priority`, unique on their network, the highest applying first, in `add_traffic_rule`, `update_traffic_rule` and the JSON-RPC methods `sdnController.addRule` and `sdnController.addNetworkRule`; a rule that has one is deleted or updated only when it is given too, in `delete_traffic_rule`, the `oldRule` of `update_traffic_rule` and the JSON-RPC methods `sdnController.deleteRule` and `sdnController.deleteNetworkRule` (PR [#10525](https://github.com/vatesfr/xen-orchestra/pull/10525))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -53,12 +54,13 @@
|
||||
- @xen-orchestra/backups minor
|
||||
- @xen-orchestra/mixins minor
|
||||
- @xen-orchestra/proxy minor
|
||||
- @xen-orchestra/qa-test minor
|
||||
- @xen-orchestra/rest-api patch
|
||||
- @xen-orchestra/vmware-explorer minor
|
||||
- @xen-orchestra/web minor
|
||||
- @xen-orchestra/web-core minor
|
||||
- xo-server minor
|
||||
- xo-server-sdn-controller patch
|
||||
- xo-server-sdn-controller minor
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -212,21 +212,26 @@ Parameters:
|
||||
- _protocol_: IP, TCP, UDP, ICMP or ARP (case-insensitive).
|
||||
- _port_: required for TCP/UDP protocol. Use `json:22` so the number is passed as an integer.
|
||||
- _allow_: true or false. If set to false the packets are dropped.
|
||||
- _priority_ (optional): the OpenFlow priority of the rule, an integer from 0 to 65535, refused when `useDirectChannel` is enabled. A rule without one has the OpenFlow default, 32768. When several rules of a network match a packet, network-wide and per-VIF rules alike, the one with the **highest** priority applies: a rule of priority 30 applies before a rule of priority 20. Two rules of a network can't be given the same priority. To delete a rule that has one, give it too.
|
||||
|
||||
:::tip
|
||||
Leave room between the priorities of your rules (for example 100, 110, 120…) rather than numbering them one after the other (101, 102, 103…): a new rule can then go between two existing ones without renumbering them.
|
||||
:::
|
||||
|
||||
Some examples:
|
||||
|
||||
<Terminal shell title="per-VIF rules">{`
|
||||
xo-cli sdnController.addRule vifId=15f1a9ec-4348-0cca-d2b8-e536db043298 direction=from ipRange=1.1.1.3 protocol=tcp port=json:4242 allow=false
|
||||
xo-cli sdnController.deleteRule vifId=15f1a9ec-4348-0cca-d2b8-e536db043298 direction=from ipRange=1.1.1.3 protocol=tcp port=json:4242
|
||||
xo-cli sdnController.addRule vifId=e9a7914a-9518-82e2-7052-42cb16cc9724 direction=to ipRange=1.1.1.2 protocol=icmp allow=false
|
||||
xo-cli sdnController.deleteRule vifId=e9a7914a-9518-82e2-7052-42cb16cc9724 direction=to ipRange=1.1.1.2 protocol=icmp
|
||||
xo-cli sdnController.addRule vifId=15f1a9ec-4348-0cca-d2b8-e536db043298 direction=from ipRange=1.1.1.3 protocol=tcp port=json:4242 allow=false priority=json:100
|
||||
xo-cli sdnController.deleteRule vifId=15f1a9ec-4348-0cca-d2b8-e536db043298 direction=from ipRange=1.1.1.3 protocol=tcp port=json:4242 priority=json:100
|
||||
xo-cli sdnController.addRule vifId=e9a7914a-9518-82e2-7052-42cb16cc9724 direction=to ipRange=1.1.1.2 protocol=icmp allow=false priority=json:110
|
||||
xo-cli sdnController.deleteRule vifId=e9a7914a-9518-82e2-7052-42cb16cc9724 direction=to ipRange=1.1.1.2 protocol=icmp priority=json:110
|
||||
`}</Terminal>
|
||||
|
||||
<Terminal shell title="network-wide rules">{`
|
||||
xo-cli sdnController.addNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=to ipRange=1.1.1.2 protocol=icmp allow=false
|
||||
xo-cli sdnController.deleteNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=to ipRange=1.1.1.2 protocol=icmp
|
||||
xo-cli sdnController.addNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=from ipRange=1.1.1.2 protocol=tcp allow=false port=json:4242
|
||||
xo-cli sdnController.deleteNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=from ipRange=1.1.1.2 protocol=tcp port=json:4242
|
||||
xo-cli sdnController.addNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=to ipRange=1.1.1.2 protocol=icmp allow=false priority=json:200
|
||||
xo-cli sdnController.deleteNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=to ipRange=1.1.1.2 protocol=icmp priority=json:200
|
||||
xo-cli sdnController.addNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=from ipRange=1.1.1.2 protocol=tcp allow=false port=json:4242 priority=json:210
|
||||
xo-cli sdnController.deleteNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=from ipRange=1.1.1.2 protocol=tcp port=json:4242 priority=json:210
|
||||
`}</Terminal>
|
||||
|
||||
### Migration path
|
||||
|
||||
@@ -31,6 +31,7 @@ POST /vifs/{id}/actions/add_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | OpenFlow priority, 0 to 65535, unique on the network, highest first. XAPI plugin only. |
|
||||
|
||||
Deletes a new rule
|
||||
POST /vifs/{id}/actions/delete_traffic_rule :
|
||||
@@ -43,6 +44,7 @@ POST /vifs/{id}/actions/delete_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | The priority of the rule, required when it has one. |
|
||||
|
||||
Creates a new rule at network level
|
||||
POST /networks/{id}/actions/add_traffic_rule :
|
||||
@@ -56,6 +58,7 @@ POST /networks/{id}/actions/add_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | OpenFlow priority, 0 to 65535, unique on the network, highest first. XAPI plugin only. |
|
||||
|
||||
Deletes a new rule at network level
|
||||
POST /networks/{id}/actions/delete_traffic_rule :
|
||||
@@ -68,3 +71,15 @@ POST /networks/{id}/actions/delete_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | The priority of the rule, required when it has one. |
|
||||
|
||||
Updates a rule at VIF or network level
|
||||
POST /vifs/{id}/actions/update_traffic_rule :
|
||||
POST /networks/{id}/actions/update_traffic_rule :
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ------------------------------------------------------------------------------------------- |
|
||||
| `oldRule` | object | Yes | Rule to update: `allow`, `direction`, `ipRange`, `protocol`, `port` and `priority`, if set. |
|
||||
| `newRule` | object | Yes | Fields to change, `priority` included. `null` removes the `port`. |
|
||||
|
||||
@@ -39,6 +39,7 @@ POST /vifs/{id}/actions/add_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | OpenFlow priority, 0 to 65535, unique on the network, highest first. XAPI plugin only. |
|
||||
|
||||
Deletes a new rule
|
||||
POST /vifs/{id}/actions/delete_traffic_rule :
|
||||
@@ -51,6 +52,7 @@ POST /vifs/{id}/actions/delete_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | The priority of the rule, required when it has one. |
|
||||
|
||||
Creates a new rule at network level
|
||||
POST /networks/{id}/actions/add_traffic_rule :
|
||||
@@ -64,6 +66,7 @@ POST /networks/{id}/actions/add_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | OpenFlow priority, 0 to 65535, unique on the network, highest first. XAPI plugin only. |
|
||||
|
||||
Deletes a new rule at network level
|
||||
POST /networks/{id}/actions/delete_traffic_rule :
|
||||
@@ -76,6 +79,18 @@ POST /networks/{id}/actions/delete_traffic_rule :
|
||||
| `ipRange` | string | Yes | IP address or range. Example: `"111.168.1.2"` or `"111.168.1.0/24"`. |
|
||||
| `port` | string | No | Port or port range. Empty string means all ports. Example: `"80"` or `"1000-2000"`. |
|
||||
| `protocol` | string | Yes | Network protocol. Common values: `"TCP"`, `"UDP"`, `"IP"` (any protocol). |
|
||||
| `priority` | number | No | The priority of the rule, required when it has one. |
|
||||
|
||||
Updates a rule at VIF or network level
|
||||
POST /vifs/{id}/actions/update_traffic_rule :
|
||||
POST /networks/{id}/actions/update_traffic_rule :
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ------------------------------------------------------------------------------------------- |
|
||||
| `oldRule` | object | Yes | Rule to update: `allow`, `direction`, `ipRange`, `protocol`, `port` and `priority`, if set. |
|
||||
| `newRule` | object | Yes | Fields to change, `priority` included. `null` removes the `port`. |
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ import { filter, find, forOwn, map, omitBy } from 'lodash'
|
||||
import { fromCallback, promisify } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { invalidParameters, objectAlreadyExists } from 'xo-common/api-errors.js'
|
||||
|
||||
import { OvsdbClient } from './protocol/ovsdb-client'
|
||||
import { PrivateNetwork } from './private-network/private-network'
|
||||
import { TlsHelper } from './utils/tls-helper'
|
||||
import { instantiateController } from './openflow-controller'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { createRestRoutes } from './rest-api'
|
||||
import { createRestRoutes, listNetworkRules } from './rest-api'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -385,13 +386,17 @@ class SDNController extends EventEmitter {
|
||||
|
||||
// ---------------- OpenFlow rules method ----------------------------------
|
||||
|
||||
const addRule = params => this._addRule(params)
|
||||
const addRule = params => {
|
||||
this._checkTrafficRulePriority(params, params.priority)
|
||||
return this._addRule(params)
|
||||
}
|
||||
addRule.description = 'Add an ACL rule to a VIF'
|
||||
addRule.params = {
|
||||
allow: { type: 'boolean' },
|
||||
direction: { type: 'string' },
|
||||
ipRange: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
priority: { type: 'integer', minimum: 0, maximum: 65535, optional: true },
|
||||
protocol: { type: 'string' },
|
||||
vifId: { type: 'string' },
|
||||
}
|
||||
@@ -403,18 +408,23 @@ class SDNController extends EventEmitter {
|
||||
direction: { type: 'string' },
|
||||
ipRange: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
priority: { type: 'integer', minimum: 0, maximum: 65535, optional: true },
|
||||
protocol: { type: 'string' },
|
||||
vifId: { type: 'string' },
|
||||
}
|
||||
deleteRule.permission = 'admin'
|
||||
|
||||
const addNetworkRule = params => this._addNetworkRule(params)
|
||||
const addNetworkRule = params => {
|
||||
this._checkTrafficRulePriority(params, params.priority)
|
||||
return this._addNetworkRule(params)
|
||||
}
|
||||
addNetworkRule.description = 'Add an ACL rule to a network'
|
||||
addNetworkRule.params = {
|
||||
allow: { type: 'boolean' },
|
||||
direction: { type: 'string' },
|
||||
ipRange: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
priority: { type: 'integer', minimum: 0, maximum: 65535, optional: true },
|
||||
protocol: { type: 'string' },
|
||||
networkId: { type: 'string' },
|
||||
}
|
||||
@@ -426,6 +436,7 @@ class SDNController extends EventEmitter {
|
||||
direction: { type: 'string' },
|
||||
ipRange: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
priority: { type: 'integer', minimum: 0, maximum: 65535, optional: true },
|
||||
protocol: { type: 'string' },
|
||||
networkId: { type: 'string' },
|
||||
}
|
||||
@@ -708,7 +719,31 @@ class SDNController extends EventEmitter {
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
async _addRule({ allow, direction, ipRange = '', port, protocol, vifId }) {
|
||||
// Run before anything is written, as the rule methods log their errors instead of throwing them
|
||||
_checkTrafficRulePriority({ networkId, vifId }, priority) {
|
||||
if (this.#staticConfig.useDirectChannel) {
|
||||
if (priority !== undefined) {
|
||||
throw invalidParameters('a traffic rule priority needs the XAPI plugin')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: throw once XO 5 and XO 6 send a priority. Until then, a rule without one has the OVS
|
||||
// default, 32768, as the rules from before priorities
|
||||
if (priority === undefined) {
|
||||
return
|
||||
}
|
||||
if (!Number.isInteger(priority) || priority < 0 || priority > 65535) {
|
||||
throw invalidParameters(`traffic rule priority must be an integer from 0 to 65535, not ${priority}`)
|
||||
}
|
||||
networkId ??= this._xo.getObject(vifId, 'VIF').$network
|
||||
const ruleWithPriority = listNetworkRules(this._xo, networkId).find(rule => rule.priority === priority)
|
||||
if (ruleWithPriority !== undefined) {
|
||||
throw objectAlreadyExists({ objectId: JSON.stringify(ruleWithPriority), objectType: 'traffic-rule' })
|
||||
}
|
||||
}
|
||||
|
||||
async _addRule({ allow, direction, ipRange = '', port, priority, protocol, vifId }) {
|
||||
const vif = this._xo.getXapiObject(this._xo.getObject(vifId, 'VIF'))
|
||||
try {
|
||||
await this._setPoolControllerIfNeeded(vif.$pool)
|
||||
@@ -718,7 +753,7 @@ class SDNController extends EventEmitter {
|
||||
const channel = await this._getOrCreateOfChannel(vif.$VM.$resident_on)
|
||||
if (client !== undefined && channel !== undefined) {
|
||||
const ofport = await client.getOfPortForVif(vif)
|
||||
await channel.addRule({ vif, allow, protocol, port, ipRange, direction, ofport })
|
||||
await channel.addRule({ vif, allow, protocol, port, ipRange, direction, priority, ofport })
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'HOST_OFFLINE') {
|
||||
@@ -739,6 +774,7 @@ class SDNController extends EventEmitter {
|
||||
port,
|
||||
ipRange,
|
||||
direction,
|
||||
priority,
|
||||
})
|
||||
if (!newVifRules.includes(stringRule)) {
|
||||
newVifRules.push(stringRule)
|
||||
@@ -759,7 +795,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async _addNetworkRule({ networkId, allow, direction, ipRange = '', port, protocol }) {
|
||||
async _addNetworkRule({ networkId, allow, direction, ipRange = '', port, priority, protocol }) {
|
||||
try {
|
||||
const network = this._xo.getXapiObject(this._xo.getObject(networkId, 'network'))
|
||||
assert(network.$PIFs.length > 0, 'Network needs to be plugged to add a rule')
|
||||
@@ -769,7 +805,11 @@ class SDNController extends EventEmitter {
|
||||
// filter matching rule (don't compare allow and cookie)
|
||||
const matchRules = networkRules.filter(rule => {
|
||||
return (
|
||||
rule.direction === direction && rule.ipRange === ipRange && rule.port === port && rule.protocol === protocol
|
||||
rule.direction === direction &&
|
||||
rule.ipRange === ipRange &&
|
||||
rule.port === port &&
|
||||
rule.priority === priority &&
|
||||
rule.protocol === protocol
|
||||
)
|
||||
})
|
||||
|
||||
@@ -795,7 +835,11 @@ class SDNController extends EventEmitter {
|
||||
// update the rule if already here (don't compare allow and cookie) to behave like OpenVSwitch
|
||||
const newNetworkRules = networkRules.filter(rule => {
|
||||
return (
|
||||
rule.protocol !== protocol || rule.port !== port || rule.ipRange !== ipRange || rule.direction !== direction
|
||||
rule.protocol !== protocol ||
|
||||
rule.port !== port ||
|
||||
rule.ipRange !== ipRange ||
|
||||
rule.direction !== direction ||
|
||||
rule.priority !== priority
|
||||
)
|
||||
})
|
||||
const newRule = {
|
||||
@@ -804,6 +848,7 @@ class SDNController extends EventEmitter {
|
||||
port,
|
||||
ipRange,
|
||||
direction,
|
||||
priority,
|
||||
cookie,
|
||||
}
|
||||
newNetworkRules.push(newRule)
|
||||
@@ -812,7 +857,7 @@ class SDNController extends EventEmitter {
|
||||
const host = this._xo.getXapiObject(this._xo.getObject(network.$PIFs[0].host, 'host'))
|
||||
const channel = await this._getOrCreateOfChannel(host)
|
||||
if (channel !== undefined) {
|
||||
await channel.addNetworkRule({ network, allow, protocol, port, ipRange, direction, cookie })
|
||||
await channel.addNetworkRule({ network, allow, protocol, port, ipRange, direction, priority, cookie })
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'HOST_OFFLINE') {
|
||||
@@ -844,7 +889,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async _deleteRule({ direction, ipRange = '', port, protocol, vifId }, updateOtherConfig = true) {
|
||||
async _deleteRule({ direction, ipRange = '', port, priority, protocol, vifId }, updateOtherConfig = true) {
|
||||
let vif = this._xo.getXapiObject(this._xo.getObject(vifId, 'VIF'))
|
||||
try {
|
||||
await this._setPoolControllerIfNeeded(vif.$pool)
|
||||
@@ -880,7 +925,11 @@ class SDNController extends EventEmitter {
|
||||
const newVifRules = JSON.parse(vifRules).filter(vifRule => {
|
||||
const rule = JSON.parse(vifRule)
|
||||
return (
|
||||
rule.protocol !== protocol || rule.port !== port || rule.ipRange !== ipRange || rule.direction !== direction
|
||||
rule.protocol !== protocol ||
|
||||
rule.port !== port ||
|
||||
rule.ipRange !== ipRange ||
|
||||
rule.direction !== direction ||
|
||||
rule.priority !== priority
|
||||
)
|
||||
})
|
||||
|
||||
@@ -906,7 +955,10 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async _deleteNetworkOfRule({ direction, ipRange = '', port, protocol, networkId }, updateOtherConfig = true) {
|
||||
async _deleteNetworkOfRule(
|
||||
{ direction, ipRange = '', port, priority, protocol, networkId },
|
||||
updateOtherConfig = true
|
||||
) {
|
||||
try {
|
||||
let network = this._xo.getXapiObject(this._xo.getObject(networkId, 'network'))
|
||||
assert(network.$PIFs.length > 0, 'Network needs to be plugged to delete a rule')
|
||||
@@ -916,7 +968,11 @@ class SDNController extends EventEmitter {
|
||||
// filter matching rule (don't compare allow and cookie)
|
||||
const matchRules = networkRules.filter(rule => {
|
||||
return (
|
||||
rule.direction === direction && rule.ipRange === ipRange && rule.port === port && rule.protocol === protocol
|
||||
rule.direction === direction &&
|
||||
rule.ipRange === ipRange &&
|
||||
rule.port === port &&
|
||||
rule.priority === priority &&
|
||||
rule.protocol === protocol
|
||||
)
|
||||
})
|
||||
|
||||
@@ -959,6 +1015,7 @@ class SDNController extends EventEmitter {
|
||||
rule.port !== port ||
|
||||
rule.ipRange !== ipRange ||
|
||||
rule.direction !== direction ||
|
||||
rule.priority !== priority ||
|
||||
rule.cookie !== cookie
|
||||
)
|
||||
})
|
||||
|
||||
@@ -23,11 +23,11 @@ export class OpenFlowPlugin {
|
||||
)
|
||||
}
|
||||
|
||||
async addRule({ vif, allow, protocol, ipRange, direction, port }) {
|
||||
async addRule({ vif, allow, protocol, ipRange, direction, port, priority }) {
|
||||
if (port) {
|
||||
port = String(port)
|
||||
}
|
||||
log.debug('addRule', { vif, allow, protocol, ipRange, direction, port })
|
||||
log.debug('addRule', { vif, allow, protocol, ipRange, direction, port, priority })
|
||||
return this.#callPluginOnAllNetwork(vif.$network, 'add-rule', {
|
||||
mac: vif.MAC,
|
||||
allow: allow ? 'true' : 'false',
|
||||
@@ -35,6 +35,8 @@ export class OpenFlowPlugin {
|
||||
ipRange,
|
||||
direction,
|
||||
port,
|
||||
// 0 is a valid priority
|
||||
priority: priority === undefined ? undefined : String(priority),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,17 +55,18 @@ export class OpenFlowPlugin {
|
||||
})
|
||||
}
|
||||
|
||||
async addNetworkRule({ network, allow, protocol, ipRange, direction, port, cookie }) {
|
||||
async addNetworkRule({ network, allow, protocol, ipRange, direction, port, priority, cookie }) {
|
||||
if (port) {
|
||||
port = String(port)
|
||||
}
|
||||
log.debug('addNetworkRule', { network, allow, protocol, ipRange, direction, port, cookie })
|
||||
log.debug('addNetworkRule', { network, allow, protocol, ipRange, direction, port, priority, cookie })
|
||||
return this.#callPluginOnAllNetwork(network, 'add-rule', {
|
||||
allow: allow ? 'true' : 'false',
|
||||
protocol,
|
||||
ipRange,
|
||||
direction,
|
||||
port,
|
||||
priority: priority === undefined ? undefined : String(priority),
|
||||
cookie,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ export class OpenFlowChannel extends EventEmitter {
|
||||
try {
|
||||
await this._coalesceConnect()
|
||||
} catch (error) {
|
||||
log.error("addRule: _coalesceConnect", {
|
||||
error
|
||||
log.error('addRule: _coalesceConnect', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -200,8 +200,8 @@ export class OpenFlowChannel extends EventEmitter {
|
||||
try {
|
||||
await this._coalesceConnect()
|
||||
} catch (error) {
|
||||
log.error("deleteRule: _coalesceConnect", {
|
||||
error
|
||||
log.error('deleteRule: _coalesceConnect', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,14 +6,25 @@ const QUERY_SYNC = { sync: { type: 'boolean', optional: true } }
|
||||
|
||||
const PARAMS_ID = { id: { type: 'string', example: 'b97f4e69-d275-4b25-9dc9-c1ac4e9b3fa5' } }
|
||||
|
||||
// only the XAPI plugin sets one
|
||||
const PRIORITY_FIELD = { type: 'number', example: 100, optional: true }
|
||||
|
||||
const RULE_FIELDS = {
|
||||
allow: { type: 'boolean', example: true },
|
||||
direction: { type: 'string', example: 'to' },
|
||||
ipRange: { type: 'string', example: '192.168.0.0/24' },
|
||||
protocol: { type: 'string', example: 'tcp' },
|
||||
port: { type: 'number', example: 443, optional: true },
|
||||
priority: PRIORITY_FIELD,
|
||||
}
|
||||
|
||||
const PRIORITY_DESCRIPTION = [
|
||||
'`priority` is the OpenFlow priority of the rule, an integer from 0 to 65535, optional with the XAPI plugin and refused without it (`useDirectChannel` in the configuration). A rule without one has the OpenFlow default, 32768.',
|
||||
'Of the rules of a network matching a packet, network and VIF rules alike, the one with the highest priority applies: a rule of priority 30 applies before a rule of priority 20.',
|
||||
'Two rules of a network cannot be given the same priority.',
|
||||
'Leave room between priorities (100, 110, 120…, not 101, 102, 103…): a rule can then go between two others without renumbering them.',
|
||||
].join(' ')
|
||||
|
||||
const BODY_UPDATE_RULE = {
|
||||
oldRule: { type: 'object', fields: RULE_FIELDS },
|
||||
newRule: {
|
||||
@@ -24,6 +35,7 @@ const BODY_UPDATE_RULE = {
|
||||
ipRange: { type: 'string', example: '10.0.0.0/8', optional: true },
|
||||
protocol: { type: 'string', example: 'tcp', optional: true },
|
||||
port: { type: 'number', example: 80, optional: true, nullable: true },
|
||||
priority: PRIORITY_FIELD,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -56,12 +68,35 @@ function parseRules(raw) {
|
||||
return raw == null ? [] : JSON.parse(raw).map(JSON.parse)
|
||||
}
|
||||
|
||||
// The rules of a network or a VIF, as the 409 of a taken priority returns them: a network rule's cookie is internal
|
||||
function listedRules(object) {
|
||||
return parseRules(object.other_config[SDN_CONTROLLER_OF_RULES_KEY]).map(({ cookie, ...rule }) => ({
|
||||
...rule,
|
||||
type: object.type,
|
||||
sourceId: object.id,
|
||||
}))
|
||||
}
|
||||
|
||||
// Not those of snapshots and templates: XAPI copies a VIF's rules with it, but they never apply
|
||||
function vmVifsOf(xo, networkId) {
|
||||
return xo
|
||||
.getObject(networkId, 'network')
|
||||
.VIFs.map(id => xo.getObject(id, 'VIF'))
|
||||
.filter(vif => xo.getObject(vif.$VM).type === 'VM')
|
||||
}
|
||||
|
||||
// The rules of a network, then those of the VIFs of its VMs
|
||||
export function listNetworkRules(xo, networkId) {
|
||||
return [xo.getObject(networkId, 'network'), ...vmVifsOf(xo, networkId)].flatMap(listedRules)
|
||||
}
|
||||
|
||||
function rulesEqual(a, b) {
|
||||
return (
|
||||
a.allow === b.allow &&
|
||||
a.direction === b.direction &&
|
||||
a.ipRange === b.ipRange &&
|
||||
a.port === b.port &&
|
||||
a.priority === b.priority &&
|
||||
a.protocol.toLowerCase() === b.protocol.toLowerCase()
|
||||
)
|
||||
}
|
||||
@@ -90,13 +125,16 @@ function ruleFromBody(req, idKey) {
|
||||
if (req.body.port != null) {
|
||||
rule.port = req.body.port
|
||||
}
|
||||
if (req.body.priority != null) {
|
||||
rule.priority = req.body.priority
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
function addRuleRoute(controller, resource) {
|
||||
return {
|
||||
endpoint: `/${resource.collection}/{id}/actions/add_traffic_rule`,
|
||||
description: `Add a traffic rule to a ${resource.type}.\n\nRequired privilege:\n - resource: ${resource.acl}, action: update:other_config`,
|
||||
description: `Add a traffic rule to a ${resource.type}.\n\n${PRIORITY_DESCRIPTION}\n\nRequired privilege:\n - resource: ${resource.acl}, action: update:other_config`,
|
||||
method: 'post',
|
||||
tags: ['sdn-controller'],
|
||||
params: PARAMS_ID,
|
||||
@@ -105,20 +143,31 @@ function addRuleRoute(controller, resource) {
|
||||
responses: [
|
||||
{ status: 204, description: 'Rule added successfully' },
|
||||
{ status: 404, description: `No ${resource.type} found for this ID` },
|
||||
{ status: 409, description: 'Another rule of the network has this priority' },
|
||||
{
|
||||
status: 422,
|
||||
description: 'Invalid rule or priority, or priority given without the XAPI plugin',
|
||||
},
|
||||
],
|
||||
middlewares: jsonAndAcl(resource.acl),
|
||||
callback: ({ req, createAction }) => {
|
||||
const rule = ruleFromBody(req, resource.idKey)
|
||||
return createAction(() => resource.addRule(controller, rule), {
|
||||
sync: req.query.sync ?? false,
|
||||
statusCode: 204,
|
||||
taskProperties: {
|
||||
name: `add ${resource.acl} traffic rule`,
|
||||
objectId: rule[resource.idKey],
|
||||
objectType: resource.type,
|
||||
params: req.body,
|
||||
return createAction(
|
||||
async () => {
|
||||
controller._checkTrafficRulePriority(rule, req.body.priority)
|
||||
await resource.addRule(controller, rule)
|
||||
},
|
||||
})
|
||||
{
|
||||
sync: req.query.sync ?? false,
|
||||
statusCode: 204,
|
||||
taskProperties: {
|
||||
name: `add ${resource.acl} traffic rule`,
|
||||
objectId: rule[resource.idKey],
|
||||
objectType: resource.type,
|
||||
params: req.body,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -167,7 +216,7 @@ function deleteRuleRoute(controller, resource) {
|
||||
function updateRuleRoute(controller, resource) {
|
||||
return {
|
||||
endpoint: `/${resource.collection}/{id}/actions/update_traffic_rule`,
|
||||
description: `Update a rule on a ${resource.type}: \`oldRule\` identifies the rule to update and must be given in full, \`newRule\` is a partial update where a field set to \`null\` is removed from the rule.\n\nRequired privilege:\n - resource: ${resource.acl}, action: update:other_config`,
|
||||
description: `Update a rule on a ${resource.type}: \`oldRule\` identifies the rule to update and must be given in full, its priority included, \`newRule\` is a partial update where a field set to \`null\` is removed from the rule.\n\n${PRIORITY_DESCRIPTION}\n\nRequired privilege:\n - resource: ${resource.acl}, action: update:other_config`,
|
||||
method: 'post',
|
||||
tags: ['sdn-controller'],
|
||||
params: PARAMS_ID,
|
||||
@@ -176,6 +225,11 @@ function updateRuleRoute(controller, resource) {
|
||||
responses: [
|
||||
{ status: 204, description: 'Rule updated successfully' },
|
||||
{ status: 404, description: `Old rule does not exist on this ${resource.type}` },
|
||||
{ status: 409, description: 'Another rule of the network has the new priority' },
|
||||
{
|
||||
status: 422,
|
||||
description: 'Invalid rule or priority, or priority given without the XAPI plugin',
|
||||
},
|
||||
],
|
||||
middlewares: jsonAndAcl(resource.acl),
|
||||
callback: ({ req, createAction }) => {
|
||||
@@ -189,6 +243,10 @@ function updateRuleRoute(controller, resource) {
|
||||
throw noSuchObject(JSON.stringify(oldRule), 'traffic-rule')
|
||||
}
|
||||
const newRule = applyRulePatch(oldRule, partialNewRule)
|
||||
// a kept priority may be shared, like the copy a clone makes of a VIF's rules
|
||||
if (newRule.priority !== oldRule.priority) {
|
||||
controller._checkTrafficRulePriority({ [resource.idKey]: id }, newRule.priority)
|
||||
}
|
||||
|
||||
await resource.deleteRule(controller, { ...oldRule, [resource.idKey]: id })
|
||||
await resource.addRule(controller, { ...newRule, [resource.idKey]: id })
|
||||
|
||||
176
packages/xo-server-sdn-controller/src/rest-api.test.js
Normal file
176
packages/xo-server-sdn-controller/src/rest-api.test.js
Normal file
@@ -0,0 +1,176 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import { strict as assert } from 'node:assert'
|
||||
import { invalidParameters, noSuchObject, objectAlreadyExists } from 'xo-common/api-errors.js'
|
||||
import { SDN_CONTROLLER_OF_RULES_KEY } from '@vates/types'
|
||||
|
||||
import createSdnController from './index.js'
|
||||
import { createRestRoutes, listNetworkRules } from './rest-api.js'
|
||||
|
||||
const noop = () => undefined
|
||||
|
||||
const SSH = { allow: false, protocol: 'TCP', port: 22, ipRange: '10.0.0.0/8', direction: 'to' }
|
||||
const WEB = { allow: true, protocol: 'TCP', port: 443, ipRange: '10.0.0.0/8', direction: 'to' }
|
||||
const NETWORK_RULE = { ...SSH, priority: 40000 }
|
||||
const VIF_RULE = { ...WEB, priority: 30000 }
|
||||
|
||||
const storeRules = rules => ({ [SDN_CONTROLLER_OF_RULES_KEY]: JSON.stringify(rules.map(rule => JSON.stringify(rule))) })
|
||||
|
||||
const NETWORK = {
|
||||
id: 'net',
|
||||
type: 'network',
|
||||
VIFs: ['vif1', 'vif2', 'vif3'],
|
||||
other_config: storeRules([{ ...NETWORK_RULE, cookie: '0x1' }]),
|
||||
}
|
||||
const VIF = {
|
||||
id: 'vif1',
|
||||
type: 'VIF',
|
||||
$VM: 'vm',
|
||||
$network: 'net',
|
||||
other_config: storeRules([VIF_RULE]),
|
||||
}
|
||||
// XAPI copies the rules of a VIF to the VIFs of the snapshots and the clones of its VM
|
||||
const SNAPSHOT_VIF = { ...VIF, id: 'vif2', $VM: 'snapshot' }
|
||||
const CLONE_VIF = { ...VIF, id: 'vif3', $VM: 'clone' }
|
||||
|
||||
function createController({ useDirectChannel = false } = {}) {
|
||||
const objects = {
|
||||
net: NETWORK,
|
||||
vif1: VIF,
|
||||
vif2: SNAPSHOT_VIF,
|
||||
vif3: CLONE_VIF,
|
||||
vm: { type: 'VM' },
|
||||
snapshot: { type: 'VM-snapshot' },
|
||||
clone: { type: 'VM' },
|
||||
}
|
||||
return createSdnController({
|
||||
xo: {
|
||||
addApiMethods: noop,
|
||||
registerRestRoutes: noop,
|
||||
getAllXapis: () => [],
|
||||
getObject: id => objects[id],
|
||||
on: noop,
|
||||
removeListener: noop,
|
||||
},
|
||||
getDataDir: async () => '/tmp',
|
||||
staticConfig: { useDirectChannel },
|
||||
})
|
||||
}
|
||||
|
||||
function findRoute(controller, method, endpoint) {
|
||||
return createRestRoutes(controller).find(route => route.method === method && route.endpoint === endpoint)
|
||||
}
|
||||
|
||||
// Runs an action route, and the task it creates at once. Returns whether the task was created.
|
||||
async function runAction(controller, endpoint, id, body) {
|
||||
let created = false
|
||||
await findRoute(controller, 'post', endpoint).callback({
|
||||
req: { params: { id }, query: { sync: true }, body },
|
||||
createAction: task => {
|
||||
created = true
|
||||
return task()
|
||||
},
|
||||
})
|
||||
return created
|
||||
}
|
||||
|
||||
// the checks run in the task: without `sync`, the route answers 202 and the error shows in the task
|
||||
function rejectsInTheTask(controller, endpoint, id, body, error) {
|
||||
return assert.rejects(runAction(controller, endpoint, id, body), actual => error.is(actual))
|
||||
}
|
||||
|
||||
const ADD = '/networks/{id}/actions/add_traffic_rule'
|
||||
const UPDATE = '/networks/{id}/actions/update_traffic_rule'
|
||||
const DELETE = '/networks/{id}/actions/delete_traffic_rule'
|
||||
|
||||
describe('a rule', () => {
|
||||
it('is found with all its fields, its priority included', async () => {
|
||||
const controller = createController()
|
||||
for (const oldRule of [SSH, { ...SSH, priority: 100 }]) {
|
||||
await rejectsInTheTask(controller, UPDATE, 'net', { oldRule, newRule: { allow: true } }, noSuchObject)
|
||||
await rejectsInTheTask(controller, DELETE, 'net', oldRule, noSuchObject)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('the priority of a rule', () => {
|
||||
it('is an integer from 0 to 65535', async () => {
|
||||
for (const priority of [-1, 1.5, 65536]) {
|
||||
await rejectsInTheTask(createController(), ADD, 'net', { ...WEB, priority }, invalidParameters)
|
||||
await rejectsInTheTask(
|
||||
createController(),
|
||||
UPDATE,
|
||||
'net',
|
||||
{ oldRule: NETWORK_RULE, newRule: { priority } },
|
||||
invalidParameters
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// XO 5 and XO 6 send none yet
|
||||
it('is optional with the XAPI plugin', async () => {
|
||||
const controller = createController()
|
||||
controller._addNetworkRule = noop
|
||||
assert.equal(await runAction(controller, ADD, 'net', WEB), true)
|
||||
})
|
||||
|
||||
it('is refused without the XAPI plugin', async () => {
|
||||
const controller = createController({ useDirectChannel: true })
|
||||
await rejectsInTheTask(controller, ADD, 'net', { ...WEB, priority: 100 }, invalidParameters)
|
||||
await rejectsInTheTask(
|
||||
controller,
|
||||
UPDATE,
|
||||
'net',
|
||||
{ oldRule: NETWORK_RULE, newRule: { priority: 100 } },
|
||||
invalidParameters
|
||||
)
|
||||
})
|
||||
|
||||
it('is unique on the network, among its rules and those of the VIFs of its VMs', async () => {
|
||||
await rejectsInTheTask(createController(), ADD, 'net', { ...WEB, priority: 30000 }, objectAlreadyExists)
|
||||
const body = { oldRule: NETWORK_RULE, newRule: { priority: 30000 } }
|
||||
await rejectsInTheTask(createController(), UPDATE, 'net', body, objectAlreadyExists)
|
||||
const endpoint = '/vifs/{id}/actions/add_traffic_rule'
|
||||
await rejectsInTheTask(createController(), endpoint, 'vif1', { ...SSH, priority: 40000 }, objectAlreadyExists)
|
||||
})
|
||||
|
||||
it('may be kept by a rule that shares it, like the copy a clone makes', async () => {
|
||||
const controller = createController()
|
||||
controller._deleteRule = noop
|
||||
controller._addRule = noop
|
||||
const body = { oldRule: VIF_RULE, newRule: { allow: false } }
|
||||
assert.equal(await runAction(controller, '/vifs/{id}/actions/update_traffic_rule', 'vif3', body), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the update of a rule', () => {
|
||||
// returns the rule added in place of the old one
|
||||
async function update(newRule) {
|
||||
const controller = createController()
|
||||
let added
|
||||
controller._deleteNetworkOfRule = noop
|
||||
controller._addNetworkRule = rule => {
|
||||
added = rule
|
||||
}
|
||||
await runAction(controller, UPDATE, 'net', { oldRule: NETWORK_RULE, newRule })
|
||||
return added
|
||||
}
|
||||
|
||||
it('keeps the priority when `newRule` leaves it out', async () => {
|
||||
assert.deepEqual(await update({ allow: true }), { ...SSH, allow: true, priority: 40000, networkId: 'net' })
|
||||
})
|
||||
|
||||
it('changes the priority', async () => {
|
||||
assert.deepEqual(await update({ priority: 100 }), { ...SSH, priority: 100, networkId: 'net' })
|
||||
})
|
||||
})
|
||||
|
||||
// those the priority of a new rule is checked against
|
||||
describe('the rules of a network', () => {
|
||||
it('are its own, without their cookie, then those of the VIFs of its VMs', () => {
|
||||
assert.deepEqual(listNetworkRules(createController()._xo, 'net'), [
|
||||
{ ...SSH, priority: 40000, type: 'network', sourceId: 'net' },
|
||||
{ ...WEB, priority: 30000, type: 'VIF', sourceId: 'vif1' },
|
||||
{ ...WEB, priority: 30000, type: 'VIF', sourceId: 'vif3' },
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user