From ee6e219c92f0e8834fc0a6fa0a72271f43f52e21 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Wed, 24 Jul 2019 22:23:06 -0400 Subject: [PATCH 01/14] Update node and VM docs for RESTfulness --- docs/manuals/api.md | 289 ++++++++++++++++++++------------------------ 1 file changed, 129 insertions(+), 160 deletions(-) diff --git a/docs/manuals/api.md b/docs/manuals/api.md index 4e201b6d..53ab0441 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -22,7 +22,7 @@ For one-time authentication, the `token` value can be specified to any API endpo The PVC API consistently accepts values (variables) as either HTTP query string arguments, or as HTTP POST form body arguments, in either GET or POST mode. -Some values are `flag_` values; these do not require a data component, and signal an option by their presence. +Some values are `` values; these do not require a data component, and signal an option by their presence. ### Data formats @@ -36,26 +36,36 @@ The PCI API consistently returns JSON bodies as its responses, with the one exce #### `/api/v1` * Methods: `GET` - * Mandatory values: N/A - * Optional values: N/A + +###### `GET` + * Mandatory values: `GET`: N/A + * Optional values: `GET`: N/A Return a JSON `message` containing the API description with HTTP return code 209. Useful for determining if the API is listening and responding properly. #### `/api/v1/auth/login` * Methods: `GET`, `POST` - * Mandatory values: `token` - * Optional values: N/A -On `GET`, return an HTTP login form accepting a token to authorize a Flask session. - -On `POST`, compare the specified token to the database and authorize a session. If this comparison fails to find a match, return a JSON `message` of `Authentication failed` and HTTP code 401. - -#### `/api/v1/auth/logout` - * Methods: `GET`, `POST` +###### `GET`: * Mandatory values: N/A * Optional values: N/A -On `GET` or `POST`, deactivate the current Flask session for the active token. +Return an HTTP login form accepting a token to authorize a Flask session. + +###### `POST`: + * Mandatory values: `token` + * Optional values: N/A + +Compare the specified `token` to the database and authorize a Flask session. + +#### `/api/v1/auth/logout` + * Methods: `GET`, `POST` + +###### `GET`/`POST` + * Mandatory values: N/A + * Optional values: N/A + +Deactivate the current Flask session for the active token. ### Node endpoints @@ -63,64 +73,57 @@ These endpoints manage PVC node state and operation. #### `/api/v1/node` * Methods: `GET` + +###### `GET` * Mandatory values: N/A * Optional values: `limit` -Return a JSON document containing information about all cluster nodes . - -If `limit` is specified, return a JSON document containing information about cluster nodes with names matching `limit` as fuzzy regex. +Return a JSON document containing information about all cluster nodes. If `limit` is specified, return a JSON document containing information about cluster nodes with names matching `limit` as fuzzy regex. #### `/api/v1/node/` * Methods: `GET` + +###### `GET` * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about ``. The output is identical to `/api/v1/node?limit=` without fuzzy regex. +Return a JSON document containing information about ``. The output is identical to `/api/v1/node?limit=` without fuzzy regex matching. -If `` is not valid, return an empty JSON document. +**NOTE:** Nodes are created automatically during daemon startup; they cannot be created by the client tools. -#### `/api/v1/node//secondary` - * Methods: `POST` +#### `/api/v1/node//coordinator-state` + * Methods: `GET`, `POST` + +###### `GET`: * Mandatory values: N/A * Optional values: N/A -Set node `` into Secondary coordinator mode. +Return the coordinator state of ``. -Attempting to `secondary` a non-primary node will return a failure. +###### `POST`: + * Mandatory values: `coordinator-state` + * Optional values: N/A -#### `/api/v1/node//primary` - * Methods: `POST` +Set node `` into the specified coordinator state. Attempting to re-set an existing state has no effect. + +Valid `coordinator-state` values are: `primary`, `secondary`. + +#### `/api/v1/node//domain-state` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: N/A -Set node `` into Primary coordinator mode. +Return the domain state of ``. -Attempting to `primary` an already-primary node will return a failure. - -#### `/api/v1/node//flush` - * Methods: `POST` - * Mandatory values: N/A +###### `POST` + * Mandatory values: `domain-state` * Optional values: N/A -Flush node `` of running VMs. This command does not wait for completion of the flush and returns immediately. +Set node `` to the specified domain state. Attempting to re-set an existing state has effect only if a previous state change did not complete fully, as this triggers a fresh change of state. -Attempting to `flush` an already flushed node will **NOT** return a failure. - -#### `/api/v1/node//unflush` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -Unflush (return to ready) node ``, restoring migrated VMs. This command does not wait for completion of the flush and returns immediately. - -Attempting to `unflush` a non-flushed node will **NOT** return a failure. - -#### `/api/v1/node//ready` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -This endpoint is an alias for `/api/v1/node//unflush`. +Valid `coordinator-state` values are: `flush`, `ready`. ### VM endpoints @@ -129,136 +132,102 @@ These endpoints manage PVC virtual machine (VM) state and operation. **NOTE:** The `` variable in all VM endpoints can be either a `name` or a `uuid`. UUIDs are used internally by PVC to track and identify VMs, but are not human-readable, so the clients treat both as equally valid and will automatically determine the `uuid` for any given `name`. #### `/api/v1/vm` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `limit` -Return a JSON document containing information about all cluster VMs . +Return a JSON document containing information about all cluster VMs. If `limit` is specified, return a JSON document containing information about VMs with names matching `limit` as fuzzy regex. -If `limit` is specified, return a JSON document containing information about VMs with names matching `limit` as fuzzy regex. +###### `POST` + * Mandatory values: `xml` + * Optional values: `node`, `selector` + +Define a new VM with Libvirt XML configuration `xml` (either single-line or human-readable multi-line). + +If `node` is specified and is valid, the VM will be assigned to `node` instead of automatically determining the target node. If `node` is specified and not valid, auto-selection occurrs instead. + +If `selector` is specified and no specific and valid `node` is specified, the automatic node determination will use `selector` to determine the optimal node instead of the default for the cluster. + +Valid `selector` values are: `mem`: the node with the least allocated VM memory; `vcpus`: the node with the least allocated VM vCPUs; `load`: the node with the least current load average; `vms`: the node with the least number of provisioned VMs. + +**NOTE:** The `POST` operation assumes that the VM resources (i.e. disks, operating system, etc.) are already created. This is equivalent to the `pvc vm define` command in the PVC CLI client. *[todo v0.6]* Creating a new VM using the provisioner uses the `POST /api/vm/` endpoint instead. #### `/api/v1/vm/` - * Methods: `GET` + * Methods: `GET`, *[todo v0.6]* `POST`, `PUT`, `DELETE` + +###### `GET` * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about ``. The output is identical to `/api/v1/vm?limit=` without fuzzy regex. +Return a JSON document containing information about ``. The output is identical to `GET /api/v1/vm?limit=` without fuzzy regex matching. -If `` is not valid, return an empty JSON document. +###### *[todo v0.6]* `POST` + * Mandatory values: `vm_template` + * Optional values: `description` -#### `/api/v1/vm//define` - * Methods: `POST` +Create a new virtual machine `` with the specified VM template `vm_template` and optional text `description`. + +###### `PUT` * Mandatory values: `xml` - * Optional values: `node`, `selector` + * Optional values: `restart` -Define a new VM with name or UUID ``. +Replace the existing Libvirt XML definition for `` with the specified Libvirt XML configuration `xml` (either single-line or human-readable multi-line). -**NOTE:** While included for consistency, the specified `` value is ignored and the values from the Libvirt XML configuration will be used instead. +If `restart` is specified, the cluster will automatically `restart` the VM with the new configuration; if not, the administrator must do so manually. -`xml` must be a valid Libvirt XML definition; human-readable, multi-line formatted definitions are fully supported. +###### `DELETE` + * Mandatory values: N/A + * Optional values: `delete_disks` -If `node` is specified and is valid, the VM will be assigned to `node` instead of automatically determining the target node. If `node` is specified and not valid, return a failure. +Forcibly stop and undefine ``. -If `selector` is specified, the automatic node determination will use `selector` to determine the optimal node instead of the default (`mem`, least allocated VM memory). If `node` is also specified, this value is ignored. +If `delete_disks` is specified, also remove all Ceph storage volumes for the VM. -#### `/api/v1/vm//modify` - * Methods: `POST` - * Mandatory values: `xml` - * Optional values: `flag_restart` +#### `/api/v1/vm//state` + * Methods: `GET`, `POST` -Replace an existing VM Libvirt XML definition for a VM with name or UUID ``. - -`xml` must be a valid Libvirt XML definition; human-readable, multi-line formatted definitions are fully supported. - -By default the cluster will not restart the VM to load the new configuration; the administrator must do so manually. - -If `flag_restart` is specified, the cluster will automatically `restart` the VM with the new configuration. - -#### `/api/v1/vm//undefine` - * Methods: `POST` +###### `GET` * Mandatory values: N/A * Optional values: N/A -Forcibly stop and undefine the VM with name or UUID ``, preserving Ceph volumes. +Return the state of ``. -#### `/api/v1/vm//remove` - * Methods: `POST` +###### `POST` + * Mandatory values: `state` + * Optional values: N/A + +Set `` to the specified state. Attempting to re-set an existing state has no effect. + +Valid `state` values are: `start`, `shutdown`, `stop`, `restart` + +**NOTE:** The `shutdown` state will attempt to gracefully shutdown the VM with a 90s timeout, after which it will forcibly `stop` the VM. + +#### `/api/v1/vm//node` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: N/A -Forcibly stop, undefine, and remove Ceph volumes of the VM with name or UUID ``. +Return the current host node and previous node, if applicable, for ``. -#### `/api/v1/vm//dump` - * Methods: `GET` +###### `POST` * Mandatory values: N/A - * Optional values: N/A + * Optional values: `node`, `selector`, `permanent` -Obtain the raw Libvirt XML configuration of the VM with name or UUID ``. +Change the current host node for ``, using live migration if possible, and using `shutdown` then `start` if not. -Return an XML document containing the Libvirt XML and HTTP code 200 on success. +If `node` is specified and is valid, the VM will be assigned to `node` instead of automatically determining the target node. If `node` is specified and not valid, auto-selection occurrs instead. -#### `/api/v1/vm//start` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A +If `node` is not specified and the VM is in migrated state, returns the VM to its previous `node`. -Start the VM with name or UUID ``. +If `selector` is specified and no specific and valid `node` is specified, the automatic node determination will use `selector` to determine the optimal node instead of the default for the cluster. -#### `/api/v1/vm//restart` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A +Valid `selector` values are: `mem`: the node with the least allocated VM memory; `vcpus`: the node with the least allocated VM vCPUs; `load`: the node with the least current load average; `vms`: the node with the least number of provisioned VMs. -Restart (`shutdown` then `start`) the VM with name or UUID ``. - -#### `/api/v1/vm//shutdown` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -Gracefully shutdown the VM with name or UUID ``. The shutdown event will time out after 90s and `stop` the VM. - -#### `/api/v1/vm//stop` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -Forcibly terminate the VM with name or UUID `` immediately. - -#### `/api/v1/vm//move` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: `node`, `selector` - -Permanently move (do not track previous node) the VM with name or UUID ``. Use Libvirt live migration if possible; otherwise `shutdown` then `start` on the new node. - -If `node` is specified and is valid, the VM will be assigned to `node` instead of automatically determining the target node. If `node` is specified and not valid, return a failure. - -If `selector` is specified, the automatic node determination will use `selector` to determine the optimal node instead of the default (`mem`, least allocated VM memory). If `node` is also specified, this value is ignored. - -#### `/api/v1/vm//migrate` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: `node`, `selector`, `flag_force` - -Temporarily move (track the previous node and migrated state) the VM with name or UUID ``. Use Libvirt live migration if possible; otherwise `shutdown` then `start` on the new node. - -If `node` is specified and is valid, the VM will be assigned to `node` instead of automatically determining the target node. If `node` is specified and not valid, return a failure. - -If `selector` is specified, the automatic node determination will use `selector` to determine the optimal node instead of the default (`mem`, least allocated VM memory). If `node` is also specified, this value is ignored. - -Attempting to `migrate` an already-migrated VM will return a failure. - -If `flag_force` is specified, migrate the VM even if it has already been migrated. The previous node value will not be replaced; e.g. if VM `test` was on `pvchv1`, then `migrate`ed to `pvchv2`, then `flag_force` `migrate`ed to `pvchv3`, the `previous_node` would still be `pvchv1`. This can be repeated indefinitely. - -#### `/api/v1/vm//unmigrate` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -Unmigrate the `migrate`ed VM with name or UUID ``, returning it to its previous node. Use Libvirt live migration if possible; otherwise `shutdown` then `start` on the previous node. - -Attempting to `unmigrate` a non-migrated VM will return a failure. +If `permanent` is specified, the PVC system will not track the previous node and the VM will not be considered migrated. ### Network endpoints @@ -278,14 +247,14 @@ If `limit` is specified, return a JSON document containing information about clu * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about ``. The output is identical to `/api/v1/network?limit=` without fuzzy regex. +Return a JSON document containing information about ``. The output is identical to `/api/v1/network?limit=` without fuzzy regex matching. If `` is not valid, return an empty JSON document. #### `/api/v1/network//add` * Methods: `POST` * Mandatory values: `vni`, `nettype` - * Optional values: `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `flag_dhcp4`, `dhcp4_start`, `dhcp4_end` + * Optional values: `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `dhcp4`, `dhcp4_start`, `dhcp4_end` Add a new virtual network with (whitespace-free) description ``. @@ -307,16 +276,16 @@ Add a new virtual network with (whitespace-free) description ``. `ip6_gateway` specifies an IP address from the `ip6_network` for the primary coordinator node to provide gateway services to the network. If `ip6_network` is specified but `ip6_gateway` is not specified or is invalid, default to `::1`. -`flag_dhcp4` specifies that DHCPv4 should be used for the IPv4 network. +`dhcp4` specifies that DHCPv4 should be used for the IPv4 network. -`dhcp4_start` specifies an IP address for the start of the DHCPv4 IP pool. If `flag_dhcp4` is specified but `dhcp4_start` is not specified or is invalid, return a failure. +`dhcp4_start` specifies an IP address for the start of the DHCPv4 IP pool. If `dhcp4` is specified but `dhcp4_start` is not specified or is invalid, return a failure. -`dhcp4_end` specifies an IP address for the end of the DHCPv4 IP pool. If `flag_dhcp4` is specified but `dhcp4_end` is not specified or is invalid, return a failure. +`dhcp4_end` specifies an IP address for the end of the DHCPv4 IP pool. If `dhcp4` is specified but `dhcp4_end` is not specified or is invalid, return a failure. #### `/api/v1/network//modify` * Methods: `POST` * Mandatory values: N/A - * Optional values: `vni`, `nettype` `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `flag_dhcp4`, `dhcp4_start`, `dhcp4_end` + * Optional values: `vni`, `nettype` `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `dhcp4`, `dhcp4_start`, `dhcp4_end` Modify the options of an existing virtual network with description ``. @@ -334,20 +303,20 @@ Remove a virtual network with description ``. #### `/api/v1/network//dhcp` * Methods: `GET` * Mandatory values: N/A - * Optional values: `limit`, `flag_static` + * Optional values: `limit`, `static` Return a JSON document containing information about all active DHCP leases in virtual network with description ``. If `limit` is specified, return a JSON document containing information about all active DHCP leases with MAC addresses matching `limit` as fuzzy regex. -If `flag_static` is specified, only return static DHCP leases. +If `static` is specified, only return static DHCP leases. #### `/api/v1/network//dhcp/` * Methods: `GET` * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about DHCP lease with MAC address `` in virtual network with description ``. The output is identical to `/api/v1/network//dhcp?limit=` without fuzzy regex. +Return a JSON document containing information about DHCP lease with MAC address `` in virtual network with description ``. The output is identical to `/api/v1/network//dhcp?limit=` without fuzzy regex matching. If `` is not valid, return an empty JSON document. @@ -385,7 +354,7 @@ If `direction` is specified and is one of `in` or `out`, return a JSON codument * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about NFTables ACL with description `` in virtual network with description ``. The output is identical to `/api/v1/network//acl?limit=` without fuzzy regex. +Return a JSON document containing information about NFTables ACL with description `` in virtual network with description ``. The output is identical to `/api/v1/network//acl?limit=` without fuzzy regex matching. If `` is not valid, return an empty JSON document. @@ -487,12 +456,12 @@ Add a new Ceph OSD to PVC node with name ``. #### `/api/v1/storage/ceph/osd//remove` * Methods: `POST` - * Mandatory values: `flag_yes_i_really_mean_it` + * Mandatory values: `yes_i_really_mean_it` * Optional values: N/A Remove a Ceph OSD device with ID `` from the storage cluster. -**NOTE:** This is a command with potentially dangerous unintended consequences that should not be scripted. To acknowledge the danger, the `flag_yes_i_really_mean_it` must be set or the endpoint will return a failure. +**NOTE:** This is a command with potentially dangerous unintended consequences that should not be scripted. To acknowledge the danger, the `yes_i_really_mean_it` must be set or the endpoint will return a failure. **WARNING:** Removing an OSD without first setting it `out` (and letting it flush) triggers an unclean PG recovery. This could potentially cause data loss if other OSDs were to fail or be removed. OSDs should not normally be removed except in the case of failed OSDs during replacement or during a replacement with a larger disk. @@ -524,7 +493,7 @@ If `limit` is specified, return a JSON document containing information about all * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/pool?limit=` without fuzzy regex. +Return a JSON document containing information about Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/pool?limit=` without fuzzy regex matching. #### `/api/v1/storage/ceph/pool//add` * Methods: `POST` @@ -537,12 +506,12 @@ Add a new Ceph RBD pool with name `` to the storage cluster. #### `/api/v1/storage/ceph/pool//remove` * Methods: `POST` - * Mandatory values: `flag_yes_i_really_mean_it` + * Mandatory values: `yes_i_really_mean_it` * Optional values: N/A Remove a Ceph RBD pool with name `` from the storage cluster. -**NOTE:** This is a command with potentially dangerous unintended consequences that should not be scripted. To acknowledge the danger, the `flag_yes_i_really_mean_it` must be set or the endpoint will return a failure. +**NOTE:** This is a command with potentially dangerous unintended consequences that should not be scripted. To acknowledge the danger, the `yes_i_really_mean_it` must be set or the endpoint will return a failure. **WARNING:** Removing an RBD pool will delete all data on that pool, including all Ceph RBD volumes on the pool. Do not run this command lightly and without ensuring the pool is safely removable first. @@ -562,7 +531,7 @@ If `limit` is specified, return a JSON document containing information about all * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&limit=` without fuzzy regex. +Return a JSON document containing information about Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&limit=` without fuzzy regex matching. #### `/api/v1/storage/ceph/volume///add` * Methods: `POST` @@ -600,7 +569,7 @@ The various limit options can be combined freely, e.g. one can specify a `volume * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&volume=&limit=` without fuzzy regex. +Return a JSON document containing information about Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&volume=&limit=` without fuzzy regex matching. #### `/api/v1/storage/ceph/volume/snapshot////add` * Methods: `POST` From eb83305ac233a6692e8d037ccc7f953a122df1f6 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 12:30:29 -0400 Subject: [PATCH 02/14] Update network docs for RESTfulness --- docs/manuals/api.md | 111 +++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/docs/manuals/api.md b/docs/manuals/api.md index 53ab0441..6f0b05d4 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -234,35 +234,23 @@ If `permanent` is specified, the PVC system will not track the previous node and These endpoints manage PVC client virtual network state and operation. #### `/api/v1/network` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `limit` -Return a JSON document containing information about all cluster networks. +Return a JSON document containing information about all cluster networks. If `limit` is specified, return a JSON document containing information about cluster networks with descriptions matching `limit` as fuzzy regex. -If `limit` is specified, return a JSON document containing information about cluster VMs with names matching `limit` as fuzzy regex. - -#### `/api/v1/network/` - * Methods: `GET` - * Mandatory values: N/A - * Optional values: N/A - -Return a JSON document containing information about ``. The output is identical to `/api/v1/network?limit=` without fuzzy regex matching. - -If `` is not valid, return an empty JSON document. - -#### `/api/v1/network//add` - * Methods: `POST` - * Mandatory values: `vni`, `nettype` +###### `POST` + * Mandatory values: `vni`, `description`, `nettype` * Optional values: `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `dhcp4`, `dhcp4_start`, `dhcp4_end` -Add a new virtual network with (whitespace-free) description ``. +Add a new virtual network to the cluster. `vni` must be a valid VNI, either a vLAN ID (for `bridged` networks) or a VXLAN ID (or `managed` networks). `description` must be a whitespace-free description of the network. -`vni` must be a valid VNI, either a vLAN ID (for `bridged` networks) or VXLAN ID (for `managed`) networks). +`nettype` must be one of the following network types: -`nettype` must be one of: - -* `bridged` for unmanaged, vLAN-based bridged networks. All optional values are ignored with this type. +* `bridged` for unmanaged, vLAN-based bridged networks. All additional optional values are ignored by this type * `managed` for PVC-managed, VXLAN-based networks. @@ -282,8 +270,16 @@ Add a new virtual network with (whitespace-free) description ``. `dhcp4_end` specifies an IP address for the end of the DHCPv4 IP pool. If `dhcp4` is specified but `dhcp4_end` is not specified or is invalid, return a failure. -#### `/api/v1/network//modify` - * Methods: `POST` +#### `/api/v1/network/` + * Methods: `GET`, `PUT`, `DELETE` + +###### `GET` + * Mandatory values: N/A + * Optional values: N/A + +Return a JSON document containing information about the virtual network with description ``. The output is identical to `/api/v1/network?limit=` without fuzzy regex matching. + +###### `PUT` * Mandatory values: N/A * Optional values: `vni`, `nettype` `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `dhcp4`, `dhcp4_start`, `dhcp4_end` @@ -293,15 +289,14 @@ All values are optional and are identical to the values for `add`. Only those va **NOTE:** Changing the `vni` or `nettype` of a virtual network is technically possible, but is not recommended. This would require updating all VMs in the network. It is usually advisable to create a new virtual network with the new VNI and type, move VMs to it, then finally remove the old virtual network. -#### `/api/v1/network//remove` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A +###### `DELETE` Remove a virtual network with description ``. -#### `/api/v1/network//dhcp` - * Methods: `GET` +#### `/api/v1/network//lease` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `limit`, `static` @@ -311,35 +306,35 @@ If `limit` is specified, return a JSON document containing information about all If `static` is specified, only return static DHCP leases. -#### `/api/v1/network//dhcp/` - * Methods: `GET` - * Mandatory values: N/A - * Optional values: N/A - -Return a JSON document containing information about DHCP lease with MAC address `` in virtual network with description ``. The output is identical to `/api/v1/network//dhcp?limit=` without fuzzy regex matching. - -If `` is not valid, return an empty JSON document. - -#### `/api/v1/network//dhcp//add` - * Methods: `POST` - * Mandatory values: `ipaddress` +###### `POST` + * Mandatory values: `macaddress`, `ipaddress` * Optional values: `hostname` -Add a new static DHCP lease for MAC address `` in virtual network with description ``. +Add a new static DHCP lease for MAC address `` in virtual network with description ``. `ipaddress` must be a valid IP address in the specified `` IPv4 netblock, and ideally outside of the DHCPv4 range. `hostname` specifies a static hostname hint for the lease. -#### `/api/v1/network//dhcp//remove` - * Methods: `POST` +#### `/api/v1/network//dhcp/` + * Methods: `GET`, `DELETE` + +###### `GET` + * Mandatory values: N/A + * Optional values: N/A + +Return a JSON document containing information about DHCP lease with MAC address `` in virtual network with description ``. The output is identical to `/api/v1/network//dhcp?limit=` without fuzzy regex matching. + +###### `DELETE` * Mandatory values: N/A * Optional values: N/A Remove a static DHCP lease for MAC address ` in virtual network with description ``. #### `/api/v1/network//acl` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `limit`, `direction` @@ -349,17 +344,7 @@ If `limit` is specified, return a JSON document containing information about all If `direction` is specified and is one of `in` or `out`, return a JSON codument listing all active NFTables ACLs in the specified direction only. If `direction` is invalid, return a failure. -#### `/api/v1/network//acl/` - * Methods: `GET` - * Mandatory values: N/A - * Optional values: N/A - -Return a JSON document containing information about NFTables ACL with description `` in virtual network with description ``. The output is identical to `/api/v1/network//acl?limit=` without fuzzy regex matching. - -If `` is not valid, return an empty JSON document. - -#### `/api/v1/network//acl//add` - * Methods: `POST` +###### `POST` * Mandatory values: `direction`, `rule` * Optional values: `order` @@ -371,8 +356,18 @@ Add a new NFTables ACL with description `` in virtual network with descript `order` specifies the order of the rule in the current chain. If not specified, the rule will be placed at the end of the rule chain. -#### `/api/v1/network//acl//remove` - * Methods: `POST` +#### `/api/v1/network//acl/` + * Methods: `GET`, `DELETE` + +###### `GET` + * Mandatory values: N/A + * Optional values: N/A + +Return a JSON document containing information about NFTables ACL with description `` in virtual network with description ``. The output is identical to `/api/v1/network//acl?limit=` without fuzzy regex matching. + +If `` is not valid, return an empty JSON document. + +###### `DELETE` * Mandatory values: N/A * Optional values: N/A From 19ae10e5825422fc6ffacc79c3ab65679c53c92d Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 12:49:02 -0400 Subject: [PATCH 03/14] Update storage docs for RESTfulness --- docs/manuals/api.md | 168 ++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 98 deletions(-) diff --git a/docs/manuals/api.md b/docs/manuals/api.md index 6f0b05d4..0aac52bc 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -384,9 +384,7 @@ These endpoints manage PVC Ceph storage cluster state and operation. This sectio * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about the current Ceph cluster status. - -The JSON element `ceph_data` contains the raw output of a `ceph status` command. +Return a JSON document containing information about the current Ceph cluster status. The JSON element `ceph_data` contains the raw output of a `ceph status` command. #### `/api/v1/storage/ceph/status` * Methods: `GET` @@ -400,57 +398,40 @@ This endpoint is an alias for `/api/v1/storage/ceph`. * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about the current Ceph cluster utilization. +Return a JSON document containing information about the current Ceph cluster utilization. The JSON element `ceph_data` contains the raw output of a `rados df` command. -The JSON element `ceph_data` contains the raw output of a `rados df` command. +#### `/api/v1/storage/ceph/cluster-option` + * Methods: `POST` + * Mandatory values: `action`, `option` + * Optional values: N/A + +Perform `action` to a global Ceph OSD `option` on the storage cluster. `action` must be either `set` or `unset`. `option` must be a valid option to the `ceph osd set/unset` commands, e.g. `noout` or `noscrub`. #### `/api/v1/storage/ceph/osd` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `limit` -Return a JSON document containing information about all Ceph OSDs in the storage cluster. +Return a JSON document containing information about all Ceph OSDs in the storage cluster. If `limit` is specified, return a JSON document containing information about all Ceph OSDs with names matching `limit` as fuzzy regex. -If `limit` is specified, return a JSON document containing information about all Ceph OSDs with names matching `limit` as fuzzy regex. +###### `POST` + * Mandatory values: `node`, `device`, `weight` + * Optional values: N/A + +Add a new Ceph OSD to PVC node with name ``. `device` must be a valid block device on the specified ``, e.g. `/dev/sdb`. `weight` must be a valid Ceph OSD weight, usually `1.0` if all OSD disks are the same size. #### `/api/v1/storage/ceph/osd/` - * Methods: `GET` + * Methods: `GET`, `DELETE` + +###### `GET` * Mandatory values: N/A * Optional values: N/A Return a JSON document containing information about Ceph OSD with ID `` in the storage cluster. Unlike other similar endpoints, this is **NOT** equivalent to any limit within the list, since that limit is based off names rather than just the ID. -#### `/api/v1/storage/ceph/osd/set` - * Methods: `POST` - * Mandatory values: `option` - * Optional values: N/A - -Set a global Ceph OSD option on the storage cluster. - -`option` must be a valid option to the `ceph osd set` command, e.g. `noout` or `noscrub`. - -#### `/api/v1/storage/ceph/osd/unset` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -Unset a global Ceph OSD option on the storage cluster. - -`option` must be a valid option to the `ceph osd unset` command, e.g. `noout` or `noscrub`. - -#### `/api/v1/storage/ceph/osd//add` - * Methods: `POST` - * Mandatory values: `device`, `weight` - * Optional values: N/A - -Add a new Ceph OSD to PVC node with name ``. - -`device` must be a valid block device on the specified ``, e.g. `/dev/sdb`. - -`weight` must be a valid Ceph OSD weight, usually `1.0` if all OSD disks are the same size. - -#### `/api/v1/storage/ceph/osd//remove` - * Methods: `POST` +###### `DELETE` * Mandatory values: `yes_i_really_mean_it` * Optional values: N/A @@ -458,49 +439,48 @@ Remove a Ceph OSD device with ID `` from the storage cluster. **NOTE:** This is a command with potentially dangerous unintended consequences that should not be scripted. To acknowledge the danger, the `yes_i_really_mean_it` must be set or the endpoint will return a failure. -**WARNING:** Removing an OSD without first setting it `out` (and letting it flush) triggers an unclean PG recovery. This could potentially cause data loss if other OSDs were to fail or be removed. OSDs should not normally be removed except in the case of failed OSDs during replacement or during a replacement with a larger disk. +**WARNING:** Removing an OSD without first setting it `out` (and letting it flush) triggers an unclean PG recovery. This could potentially cause data loss if other OSDs were to fail or be removed. OSDs should not normally be removed except in the case of failed OSDs during replacement or during a replacement with a larger disk. For more information please see the Ceph documentation. -#### `/api/v1/storage/ceph/osd//in` - * Methods: `POST` +#### `/api/v1/storage/ceph/osd//state` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: N/A -Set in (active) a Ceph OSD device with ID `` in the storage cluster. +Return the state state of OSD ``. -#### `/api/v1/storage/ceph/osd//out` - * Methods: `POST` - * Mandatory values: N/A +###### `POST` + * Mandatory values: `state` * Optional values: N/A -Set out (inactive) a Ceph OSD device with ID `` in the storage cluster. +Set a Ceph OSD device with ID `` to `state`. `state` must be either `in` or `out`. #### `/api/v1/storage/ceph/pool` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `limit` -Return a JSON document containing information about all Ceph RBD pools in the storage cluster. +Return a JSON document containing information about all Ceph RBD pools in the storage cluster. If `limit` is specified, return a JSON document containing information about all Ceph RBD pools with names matching `limit` as fuzzy regex. -If `limit` is specified, return a JSON document containing information about all Ceph RBD pools with names matching `limit` as fuzzy regex. +###### `POST` + * Mandatory values: `pool`, `pgs` + * Optional values: N/A + +Add a new Ceph RBD pool with name `` to the storage cluster. `pgs` must be a valid number of Placement Groups for the pool, taking into account the number of OSDs and the replication of the pool (`copies=3`). `256` is a safe and sane number of PGs for 3 nodes and 2 disks per node. This value can be grown later via `ceph` commands as required. #### `/api/v1/storage/ceph/pool/` - * Methods: `GET` + * Methods: `GET`, `DELETE` + +###### `GET` * Mandatory values: N/A * Optional values: N/A Return a JSON document containing information about Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/pool?limit=` without fuzzy regex matching. -#### `/api/v1/storage/ceph/pool//add` - * Methods: `POST` - * Mandatory values: `pgs` - * Optional values: N/A - -Add a new Ceph RBD pool with name `` to the storage cluster. - -`pgs` must be a valid number of Placement Groups for the pool, taking into account the number of OSDs and the replication of the pool (`copies=3`). `256` is a safe number for 3 nodes and 2 disks per node. This value can be grown later via `ceph` commands as required. - -#### `/api/v1/storage/ceph/pool//remove` - * Methods: `POST` +###### `DELETE` * Mandatory values: `yes_i_really_mean_it` * Optional values: N/A @@ -511,71 +491,63 @@ Remove a Ceph RBD pool with name `` from the storage cluster. **WARNING:** Removing an RBD pool will delete all data on that pool, including all Ceph RBD volumes on the pool. Do not run this command lightly and without ensuring the pool is safely removable first. #### `/api/v1/storage/ceph/volume` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `pool`, `limit` -Return a JSON document containing information about all Ceph RBD volumes in the storage cluster. +Return a JSON document containing information about all Ceph RBD volumes in the storage cluster. If `pool` is specified, return a JSON document containing information about all Ceph RBD volumes in Ceph RBD pool with name `pool`. If `limit` is specified, return a JSON document containing information about all Ceph RBD volumes with names matching `limit` as fuzzy regex. -If `pool` is specified, return a JSON document containing information about all Ceph RBD volumes in Ceph RBD pool with name `pool`. +###### `POST` + * Mandatory values: `pool`, `size` + * Optional values: N/A -If `limit` is specified, return a JSON document containing information about all Ceph RBD volumes with names matching `limit` as fuzzy regex. +Add a new Ceph RBD volume with name `` to Ceph RBD pool with name ``. `size` must be a valid size, in bytes or a single-character metric prefix of bytes, e.g. `1073741824` (1GB), `4096M`, or `20G`. #### `/api/v1/storage/ceph/volume//` - * Methods: `GET` + * Methods: `GET`, `DELETE` + +###### `GET` * Mandatory values: N/A * Optional values: N/A Return a JSON document containing information about Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&limit=` without fuzzy regex matching. -#### `/api/v1/storage/ceph/volume///add` - * Methods: `POST` - * Mandatory values: `size` - * Optional values: N/A - -Add a new Ceph RBD volume with name `` to Ceph RBD pool with name ``. - -`size` must be a valid size, in bytes or a single-character metric prefix of bytes, e.g. `1073741824` (1GB), `4096M`, or `2G`. - -#### `/api/v1/storage/ceph/volume///remove` - * Methods: `POST` +###### `DELETE` * Mandatory values: N/A * Optional values: N/A Remove a Ceph RBD volume with name `` from Ceph RBD pool ``. #### `/api/v1/storage/ceph/volume/snapshot` - * Methods: `GET` + * Methods: `GET`, `POST` + +###### `GET` * Mandatory values: N/A * Optional values: `pool`, `volume`, `limit` -Return a JSON document containing information about all Ceph RBD volume snapshots in the storage cluster. - -If `pool` is specified, return a JSON document containing information about all Ceph RBD volume snapshots in Ceph RBD pool with name `pool`. - -If `volume` is specified, return a JSON document containing information about all Ceph RBD volume snapshots of Ceph RBD volume with name `volume`. - -If `limit` is specified, return a JSON document containing information about all Ceph RBD volume snapshots with names matching `limit` as fuzzy regex. +Return a JSON document containing information about all Ceph RBD volume snapshots in the storage cluster. If `pool` is specified, return a JSON document containing information about all Ceph RBD volume snapshots in Ceph RBD pool with name `pool`. If `volume` is specified, return a JSON document containing information about all Ceph RBD volume snapshots of Ceph RBD volume with name `volume`. If `limit` is specified, return a JSON document containing information about all Ceph RBD volume snapshots with names matching `limit` as fuzzy regex. The various limit options can be combined freely, e.g. one can specify a `volume` without `pool`, which would match all snapshots of the named volume(s) regardless of pool, or a `pool` and `limit` without a `volume`, which would match all named snapshots on any volume in `pool`. +###### `POST` + * Mandatory values: `snapshot`, `pool`, `volume` + * Optional values: N/A + +Add a new Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` on Ceph RBD pool with name ``. + #### `/api/v1/storage/ceph/volume/snapshot///` - * Methods: `GET` + * Methods: `GET`, `DELETE` + +###### `GET` * Mandatory values: N/A * Optional values: N/A Return a JSON document containing information about Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&volume=&limit=` without fuzzy regex matching. -#### `/api/v1/storage/ceph/volume/snapshot////add` - * Methods: `POST` +###### `DELETE` * Mandatory values: N/A * Optional values: N/A -Add a new Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` on Ceph RBD pool with name ``. - -#### `/api/v1/storage/ceph/volume/snapshot////remove` - * Methods: `POST` - * Mandatory values: N/A - * Optional values: N/A - -Remove a Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` on Ceph RBD pool with name ``. +Remove a Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` on Ceph RBD pool with name ``. From bcd48648b2d4c036427fe923f320cb7ad02295fd Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 14:33:50 -0400 Subject: [PATCH 04/14] Add is_migrated check function --- client-common/vm.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client-common/vm.py b/client-common/vm.py index 9a899745..3a2ecbcb 100644 --- a/client-common/vm.py +++ b/client-common/vm.py @@ -106,6 +106,20 @@ def getDomainName(zk_conn, domain): # # Direct functions # +def is_migrated(zk_conn, domain): + # Validate that VM exists in cluster + dom_uuid = getDomainUUID(zk_conn, domain) + if not dom_uuid: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + last_node = zkhandler.readdata(zk_conn, '/domains/{}/lastnode'.format(dom_uuid)) + common.stopZKConnection(zk_conn) + if last_node: + return True + else + return False + def define_vm(zk_conn, config_data, target_node, selector): # Parse the XML data try: From dff1c68f6e38a7ca2d7cbad92e47f914bd6e1e1c Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 15:42:17 -0400 Subject: [PATCH 05/14] RESTify the VM functions and enable debug mode --- client-api/api_lib/pvcapi.py | 7 + client-api/pvc-api.py | 388 +++++++++++++++------------------ client-api/pvc-api.sample.yaml | 2 + docs/manuals/api.md | 12 +- 4 files changed, 190 insertions(+), 219 deletions(-) diff --git a/client-api/api_lib/pvcapi.py b/client-api/api_lib/pvcapi.py index 5a13c329..ae2b4c0d 100755 --- a/client-api/api_lib/pvcapi.py +++ b/client-api/api_lib/pvcapi.py @@ -117,6 +117,13 @@ def node_ready(node): # # VM functions # +def vm_is_migrated(vm): + """ + Determine if a VM is migrated or not + """ + zk_conn = pvc_common.startZKConnection(config['coordinators']) + return pvc_vm.is_migrated(zk_conn, vm) + def vm_list(node=None, state=None, limit=None, is_fuzzy=True): """ Return a list of VMs with limit LIMIT. diff --git a/client-api/pvc-api.py b/client-api/pvc-api.py index e7c89fdc..9118507b 100755 --- a/client-api/pvc-api.py +++ b/client-api/pvc-api.py @@ -29,9 +29,6 @@ import gevent.pywsgi import api_lib.pvcapi as pvcapi -api = flask.Flask(__name__) -api.config['DEBUG'] = True - # Parse the configuration file try: pvc_config_file = os.environ['PVC_CONFIG_FILE'] @@ -52,6 +49,7 @@ except Exception as e: try: # Create the config object config = { + 'debug': o_config['pvc']['debug'], 'coordinators': o_config['pvc']['coordinators'], 'listen_address': o_config['pvc']['api']['listen_address'], 'listen_port': int(o_config['pvc']['api']['listen_port']), @@ -69,6 +67,11 @@ except Exception as e: print('ERROR: {}.'.format(e)) exit(1) +api = flask.Flask(__name__) + +if config['debug']: + api.config['DEBUG'] = True + if config['auth_enabled']: api.config["SECRET_KEY"] = config['auth_secret_key'] @@ -148,245 +151,202 @@ def api_node(): @api.route('/api/v1/node/', methods=['GET']) @authenticator -def api_node_info(node): +def api_node_root(node): """ Return information about node NODE. """ # Same as specifying /node?limit=NODE return pvcapi.node_list(node) -@api.route('/api/v1/node//secondary', methods=['POST']) -@authenticator -def api_node_secondary(node): - """ - Take NODE out of primary router mode. - """ - return pvcapi.node_secondary(node) -@api.route('/api/v1/node//primary', methods=['POST']) +@api.route('/api/v1/node//coordinator-state', methods=['GET', 'POST']) @authenticator -def api_node_primary(node): +def api_node_coordinator_state(node): """ - Set NODE to primary router mode. + Manage NODE coordinator state. """ - return pvcapi.node_primary(node) -@api.route('/api/v1/node//flush', methods=['POST']) -@authenticator -def api_node_flush(node): - """ - Flush NODE of running VMs. - """ - return pvcapi.node_flush(node) + if flask.request.method == 'GET': + return "Test", 200 -@api.route('/api/v1/node//unflush', methods=['POST']) -@api.route('/api/v1/node//ready', methods=['POST']) + if flask.request.method == 'POST': + if not coordinator-state in flask.request.values: + flask.abort(400) + new_state = flask.request.values['coordinator-state'] + if new_state == 'primary': + return pvcapi.node_primary(node) + if new_state == 'secondary': + return pvcapi.node_secondary(node) + flask.abort(400) + +@api.route('/api/v1/node//domain-state', methods=['GET', 'POST']) @authenticator -def api_node_ready(node): +def api_node_domain_state(node): """ - Restore NODE to active service. + Manage NODE domain state. """ - return pvcapi.node_ready(node) + + if flask.request.method == 'GET': + return "Test", 200 + + if flask.request.method == 'POST': + if not domain-state in flask.request.values: + flask.abort(400) + new_state = flask.request.values['domain-state'] + if new_state == 'ready': + return pvcapi.node_ready(node) + if new_state == 'flush': + return pvcapi.node_flush(node) + flask.abort(400) # # VM endpoints # -@api.route('/api/v1/vm', methods=['GET']) +@api.route('/api/v1/vm', methods=['GET', 'POST']) @authenticator def api_vm(): - """ - Return a list of VMs with limit LIMIT. - """ - # Get node limit - if 'node' in flask.request.values: - node = flask.request.values['node'] - else: - node = None + if flask.request.method == 'GET': + # Get node limit + if 'node' in flask.request.values: + node = flask.request.values['node'] + else: + node = None + + # Get state limit + if 'state' in flask.request.values: + state = flask.request.values['state'] + else: + state = None + + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + return pvcapi.vm_list(node, state, limit) - # Get state limit - if 'state' in flask.request.values: - state = flask.request.values['state'] - else: - state = None + if flask.request.method == 'POST': + # Get XML data + if 'xml' in flask.request.values: + libvirt_xml = flask.request.values['xml'] + else: + return flask.jsonify({"message":"ERROR: A Libvirt XML document must be specified."}), 400 + + # Get node name + if 'node' in flask.request.values: + node = flask.request.values['node'] + else: + node = None + + # Get target selector + if 'selector' in flask.request.values: + selector = flask.request.values['selector'] + else: + selector = None + + return pvcapi.vm_define(vm, libvirt_xml, node, selector) - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None - - return pvcapi.vm_list(node, state, limit) - -@api.route('/api/v1/vm/', methods=['GET']) +@api.route('/api/v1/vm/', methods=['GET', 'POST', 'PUT', 'DELETE']) @authenticator -def api_vm_info(vm): - """ - Get information about a virtual machine named VM. - """ - # Same as specifying /vm?limit=VM - return pvcapi.vm_list(None, None, vm, is_fuzzy=False) +def api_vm_root(vm): + if flask.request.method == 'GET': + # Same as specifying /vm?limit=VM + return pvcapi.vm_list(None, None, vm, is_fuzzy=False) -# TODO: #22 -#@api.route('/api/v1/vm//add', methods=['POST']) + if flask.request.method == 'POST': + # TODO: #22 + flask.abort(501) + + if flask.request.method == 'PUT': + libvirt_xml = flask.request.data + + if 'restart' in flask.request.values and flask.request.values['restart']: + flag_restart = True + else: + flag_restart = False + + return pvcapi.vm_modify(vm, flag_restart, libvirt_xml) + + if flask.request.method == 'DELETE': + if 'delete_disks' in flask.request.values and flask.request.values['delete_disks']: + return pvcapi.vm_remove(vm) + else: + return pvcapi.vm_undefine(vm) + +#@api.route('/api/v1/vm//dump', methods=['GET']) #@authenticator -#def api_vm_add(vm): +#def api_vm_dump(vm): # """ -# Add a virtual machine named VM. +# Dump the Libvirt XML configuration of a virtual machine named VM. # """ -# return pvcapi.vm_add() +# return pvcapi.vm_dump(vm) -@api.route('/api/v1/vm//define', methods=['POST']) +@api.route('/api/v1/vm//state', methods=['GET', 'POST']) @authenticator -def api_vm_define(vm): - """ - Define a virtual machine named VM from Libvirt XML. - """ - # Get XML data - if 'xml' in flask.request.values: - libvirt_xml = flask.request.values['xml'] - else: - return flask.jsonify({"message":"ERROR: A Libvirt XML document must be specified."}), 520 +def api_vm_state(vm): + if flask.request.method == 'GET': + return "Test", 200 - # Get node name - if 'node' in flask.request.values: - node = flask.request.values['node'] - else: - node = None + if flask.request.method == 'POST': + if not state in flask.request.values: + flask.abort(400) + new_state = flask.request.values['state'] + if new_state == 'start': + return pvcapi.vm_start(vm) + if new_state == 'shutdown': + return pvcapi.vm_shutdown(vm) + if new_state == 'stop': + return pvcapi.vm_stop(vm) + if new_state == 'restart': + return pvcapi.vm_restart(vm) + flask.abort(400) - # Get target selector - if 'selector' in flask.request.values: - selector = flask.request.values['selector'] - else: - selector = None - - return pvcapi.vm_define(vm, libvirt_xml, node, selector) - -@api.route('/api/v1/vm//modify', methods=['POST']) +@api.route('/api/v1/vm//node', methods=['GET', 'POST']) @authenticator -def api_vm_modify(vm): - """ - Modify an existing virtual machine named VM from Libvirt XML. - """ - # Get XML from the POST body - libvirt_xml = flask.request.data +def api_vm_node(vm): + if flask.request.method == 'GET': + return "Test", 200 + + if flask.request.method == 'POST': + if 'action' in flask.request.values: + action = flask.request.values['action'] + else: + flask.abort(400) + + # Get node name + if 'node' in flask.request.values: + node = flask.request.values['node'] + else: + node = None + # Get target selector + if 'selector' in flask.request.values: + selector = flask.request.values['selector'] + else: + selector = None + # Get permanent flag + if 'permanent' in flask.request.values and flask.request.values['permanent']: + flag_permanent = True + else: + flag_permanent = False + # Get force flag + if 'force' in flask.request.values and flask.request.values['force']: + flag_force = True + else: + flag_force = False - # Get node name - if 'flag_restart' in flask.request.values: - flag_restart = flask.request.values['flag_restart'] - else: - flag_restart = None + # Check if VM is presently migrated + is_migrated = pvcapi.vm_is_migrated(vm) - return pvcapi.vm_modify(vm, flag_restart, libvirt_xml) + if action == 'migrate' and not flag_permanent: + return pvcapi.vm_migrate(vm, node, selector, flag_force) + if action == 'migrate' and flag_permanent: + return pvcapi.vm_move(vm, node, selector) + if action == 'unmigrate' and is_migrated: + return pvcapi.vm_unmigrate(vm) -@api.route('/api/v1/vm//undefine', methods=['POST']) -@authenticator -def api_vm_undefine(vm): - """ - Undefine a virtual machine named VM. - """ - return pvcapi.vm_undefine(vm) + flask.abort(400) -@api.route('/api/v1/vm//remove', methods=['POST']) -@authenticator -def api_vm_remove(vm): - """ - Remove a virtual machine named VM including all disks. - """ - return pvcapi.vm_remove(vm) - -@api.route('/api/v1/vm//dump', methods=['GET']) -@authenticator -def api_vm_dump(vm): - """ - Dump the Libvirt XML configuration of a virtual machine named VM. - """ - return pvcapi.vm_dump(vm) - -@api.route('/api/v1/vm//start', methods=['POST']) -@authenticator -def api_vm_start(vm): - """ - Start a virtual machine named VM. - """ - return pvcapi.vm_start(vm) - -@api.route('/api/v1/vm//restart', methods=['POST']) -@authenticator -def api_vm_restart(vm): - """ - Restart a virtual machine named VM. - """ - return pvcapi.vm_restart(vm) - -@api.route('/api/v1/vm//shutdown', methods=['POST']) -@authenticator -def api_vm_shutdown(vm): - """ - Shutdown a virtual machine named VM. - """ - return pvcapi.vm_shutdown(vm) - -@api.route('/api/v1/vm//stop', methods=['POST']) -@authenticator -def api_vm_stop(vm): - """ - Forcibly stop a virtual machine named VM. - """ - return pvcapi.vm_stop(vm) - -@api.route('/api/v1/vm//move', methods=['POST']) -@authenticator -def api_vm_move(vm): - """ - Move a virtual machine named VM to another node. - """ - # Get node name - if 'node' in flask.request.values: - node = flask.request.values['node'] - else: - node = None - - # Get target selector - if 'selector' in flask.request.values: - selector = flask.request.values['selector'] - else: - selector = None - - return pvcapi.vm_move(vm, node, selector) - -@api.route('/api/v1/vm//migrate', methods=['POST']) -@authenticator -def api_vm_migrate(vm): - """ - Temporarily migrate a virtual machine named VM to another node. - """ - # Get node name - if 'node' in flask.request.values: - node = flask.request.values['node'] - else: - node = None - - # Get target selector - if 'selector' in flask.request.values: - selector = flask.request.values['selector'] - else: - selector = None - - # Get target selector - if 'flag_force' in flask.request.values: - flag_force = True - else: - flag_force = False - - return pvcapi.vm_migrate(vm, node, selector, flag_force) - -@api.route('/api/v1/vm//unmigrate', methods=['POST']) -@authenticator -def api_vm_unmigrate(vm): - """ - Unmigrate a migrated virtual machine named VM. - """ - return pvcapi.vm_move(vm) # # Network endpoints @@ -982,13 +942,15 @@ def api_ceph_volume_snapshot_remove(pool, volume, snapshot): # # Entrypoint # -if config['ssl_enabled']: - # Run the WSGI server with SSL - http_server = gevent.pywsgi.WSGIServer((config['listen_address'], config['listen_port']), api, - keyfile=config['ssl_key_file'], certfile=config['ssl_cert_file']) +if config['debug']: + api.run(config['listen_address'], config['listen_port']) else: - # Run the ?WSGI server without SSL - http_server = gevent.pywsgi.WSGIServer((config['listen_address'], config['listen_port']), api) + if config['ssl_enabled']: + # Run the WSGI server with SSL + http_server = gevent.pywsgi.WSGIServer((config['listen_address'], config['listen_port']), api, keyfile=config['ssl_key_file'], certfile=config['ssl_cert_file']) + else: + # Run the ?WSGI server without SSL + http_server = gevent.pywsgi.WSGIServer((config['listen_address'], config['listen_port']), api) -print('Starting PyWSGI server at {}:{} with SSL={}, Authentication={}'.format(config['listen_address'], config['listen_port'], config['ssl_enabled'], config['auth_enabled'])) -http_server.serve_forever() + print('Starting PyWSGI server at {}:{} with SSL={}, Authentication={}'.format(config['listen_address'], config['listen_port'], config['ssl_enabled'], config['auth_enabled'])) + http_server.serve_forever() diff --git a/client-api/pvc-api.sample.yaml b/client-api/pvc-api.sample.yaml index 13e72eb1..a44f0078 100644 --- a/client-api/pvc-api.sample.yaml +++ b/client-api/pvc-api.sample.yaml @@ -8,6 +8,8 @@ # Copy this example to /etc/pvc/pvc-api.conf and edit to your needs pvc: + # debug: Enable/disable API debug mode + debug: True # coordinators: The list of cluster coordinator hostnames coordinators: - pvc-hv1 diff --git a/docs/manuals/api.md b/docs/manuals/api.md index 0aac52bc..dd690879 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -214,20 +214,20 @@ Valid `state` values are: `start`, `shutdown`, `stop`, `restart` Return the current host node and previous node, if applicable, for ``. ###### `POST` - * Mandatory values: N/A - * Optional values: `node`, `selector`, `permanent` + * Mandatory values: `action` + * Optional values: `node`, `selector`, `permanent`, `force` -Change the current host node for ``, using live migration if possible, and using `shutdown` then `start` if not. +Change the current host node for `` by `action`, using live migration if possible, and using `shutdown` then `start` if not. `action` must be either `migrate` or `unmigrate`. If `node` is specified and is valid, the VM will be assigned to `node` instead of automatically determining the target node. If `node` is specified and not valid, auto-selection occurrs instead. -If `node` is not specified and the VM is in migrated state, returns the VM to its previous `node`. - If `selector` is specified and no specific and valid `node` is specified, the automatic node determination will use `selector` to determine the optimal node instead of the default for the cluster. Valid `selector` values are: `mem`: the node with the least allocated VM memory; `vcpus`: the node with the least allocated VM vCPUs; `load`: the node with the least current load average; `vms`: the node with the least number of provisioned VMs. -If `permanent` is specified, the PVC system will not track the previous node and the VM will not be considered migrated. +If `permanent` is specified, the PVC system will not track the previous node and the VM will not be considered migrated. This is equivalent to the `pvc vm move` CLI command. + +If `force` is specified, and the VM has been previously migrated, force through a new migration to the selected target and do not update the previous node value. ### Network endpoints From 75f80fa4bd0881e126a1a5ee4ba843163832f4b3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 15:44:29 -0400 Subject: [PATCH 06/14] Include XML output in VM information --- client-common/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client-common/common.py b/client-common/common.py index 3f058305..dd23ba0a 100644 --- a/client-common/common.py +++ b/client-common/common.py @@ -200,7 +200,8 @@ def getInformationFromXML(zk_conn, uuid): 'emulator': domain_emulator, 'features': domain_features, 'disks': domain_disks, - 'controllers': domain_controllers + 'controllers': domain_controllers, + 'xml': parsed_xml } return domain_information From f4d63b908878c5e34a6a2d57156836601f4d0f63 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 15:45:31 -0400 Subject: [PATCH 07/14] Remove vm_dump API function --- client-api/api_lib/pvcapi.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/client-api/api_lib/pvcapi.py b/client-api/api_lib/pvcapi.py index ae2b4c0d..acd1bc59 100755 --- a/client-api/api_lib/pvcapi.py +++ b/client-api/api_lib/pvcapi.py @@ -219,20 +219,6 @@ def vm_remove(name): } return flask.jsonify(output), retcode -def vm_dump(name): - """ - Dump a VM Libvirt XML configuration. - """ - zk_conn = pvc_common.startZKConnection(config['coordinators']) - retflag, retdata = pvc_vm.dump_vm(zk_conn, name) - if retflag: - retcode = 200 - else: - retcode = 510 - - pvc_common.stopZKConnection(zk_conn) - return retdata, retcode - def vm_start(name): """ Start a VM in the PVC cluster. From fb40ef5b043b65f721060c69e77f8b7f59f852b1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 15:45:45 -0400 Subject: [PATCH 08/14] Fix typo in common --- client-common/vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-common/vm.py b/client-common/vm.py index 3a2ecbcb..39ea34fb 100644 --- a/client-common/vm.py +++ b/client-common/vm.py @@ -117,7 +117,7 @@ def is_migrated(zk_conn, domain): common.stopZKConnection(zk_conn) if last_node: return True - else + else: return False def define_vm(zk_conn, config_data, target_node, selector): From 8071fb87d759c3d2a970ef33ed43210759e083b7 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 16:29:29 -0400 Subject: [PATCH 09/14] Include XML in domain information --- client-common/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-common/common.py b/client-common/common.py index dd23ba0a..86db2b09 100644 --- a/client-common/common.py +++ b/client-common/common.py @@ -201,7 +201,7 @@ def getInformationFromXML(zk_conn, uuid): 'features': domain_features, 'disks': domain_disks, 'controllers': domain_controllers, - 'xml': parsed_xml + 'xml': lxml.etree.tostring(parsed_xml, encoding='ascii', method='xml').decode().replace('\"', '\'') } return domain_information From 1d944f56270020d05331628aebf77c8fd3290f78 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 16:46:03 -0400 Subject: [PATCH 10/14] Fully remove dump endpoing; data is now in output --- client-api/pvc-api.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client-api/pvc-api.py b/client-api/pvc-api.py index 9118507b..72e8f9f7 100755 --- a/client-api/pvc-api.py +++ b/client-api/pvc-api.py @@ -274,14 +274,6 @@ def api_vm_root(vm): else: return pvcapi.vm_undefine(vm) -#@api.route('/api/v1/vm//dump', methods=['GET']) -#@authenticator -#def api_vm_dump(vm): -# """ -# Dump the Libvirt XML configuration of a virtual machine named VM. -# """ -# return pvcapi.vm_dump(vm) - @api.route('/api/v1/vm//state', methods=['GET', 'POST']) @authenticator def api_vm_state(vm): From 375bbf4b82e6ee744df6e21b4979d09386bcf76f Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 25 Jul 2019 16:50:17 -0400 Subject: [PATCH 11/14] Fix quoting bugs in API --- client-api/pvc-api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client-api/pvc-api.py b/client-api/pvc-api.py index 72e8f9f7..7405e0b0 100755 --- a/client-api/pvc-api.py +++ b/client-api/pvc-api.py @@ -170,7 +170,7 @@ def api_node_coordinator_state(node): return "Test", 200 if flask.request.method == 'POST': - if not coordinator-state in flask.request.values: + if not 'coordinator-state' in flask.request.values: flask.abort(400) new_state = flask.request.values['coordinator-state'] if new_state == 'primary': @@ -190,7 +190,7 @@ def api_node_domain_state(node): return "Test", 200 if flask.request.method == 'POST': - if not domain-state in flask.request.values: + if not 'domain-state' in flask.request.values: flask.abort(400) new_state = flask.request.values['domain-state'] if new_state == 'ready': @@ -281,7 +281,7 @@ def api_vm_state(vm): return "Test", 200 if flask.request.method == 'POST': - if not state in flask.request.values: + if not 'state' in flask.request.values: flask.abort(400) new_state = flask.request.values['state'] if new_state == 'start': From d43ced176fa9a37ede6cdce6fd12d66a31c4dd66 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 26 Jul 2019 09:41:17 -0400 Subject: [PATCH 12/14] RESTify the network endpoints; remove some cruft --- client-api/api_lib/pvcapi.py | 11 +- client-api/pvc-api.py | 626 ++++++++++++++--------------------- docs/manuals/api.md | 30 +- 3 files changed, 274 insertions(+), 393 deletions(-) diff --git a/client-api/api_lib/pvcapi.py b/client-api/api_lib/pvcapi.py index acd1bc59..7ddaf084 100755 --- a/client-api/api_lib/pvcapi.py +++ b/client-api/api_lib/pvcapi.py @@ -376,14 +376,15 @@ def net_add(vni, description, nettype, domain, } return flask.jsonify(output), retcode -def net_modify(vni, description, nettype, domain, - ip4_network, ip4_gateway, ip6_network, ip6_gateway, +def net_modify(vni, description, domain, + ip4_network, ip4_gateway, + ip6_network, ip6_gateway, dhcp4_flag, dhcp4_start, dhcp4_end): """ Modify a virtual client network in the PVC cluster. """ zk_conn = pvc_common.startZKConnection(config['coordinators']) - retflag, retmsg = pvc_network.add_network(zk_conn, vni, description, nettype, domain, + retflag, retmsg = pvc_network.modify_network(zk_conn, vni, description, domain, ip4_network, ip4_gateway, ip6_network, ip6_gateway, dhcp4_flag, dhcp4_start, dhcp4_end) if retflag: @@ -397,12 +398,12 @@ def net_modify(vni, description, nettype, domain, } return flask.jsonify(output), retcode -def net_remove(description): +def net_remove(network): """ Remove a virtual client network from the PVC cluster. """ zk_conn = pvc_common.startZKConnection(config['coordinators']) - retflag, retmsg = pvc_network.remove_network(zk_conn, description) + retflag, retmsg = pvc_network.remove_network(zk_conn, network) if retflag: retcode = 200 else: diff --git a/client-api/pvc-api.py b/client-api/pvc-api.py index 7405e0b0..45408437 100755 --- a/client-api/pvc-api.py +++ b/client-api/pvc-api.py @@ -138,9 +138,6 @@ def api_auth_logout(): @api.route('/api/v1/node', methods=['GET']) @authenticator def api_node(): - """ - Return a list of nodes with limit LIMIT. - """ # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] @@ -152,9 +149,6 @@ def api_node(): @api.route('/api/v1/node/', methods=['GET']) @authenticator def api_node_root(node): - """ - Return information about node NODE. - """ # Same as specifying /node?limit=NODE return pvcapi.node_list(node) @@ -162,10 +156,6 @@ def api_node_root(node): @api.route('/api/v1/node//coordinator-state', methods=['GET', 'POST']) @authenticator def api_node_coordinator_state(node): - """ - Manage NODE coordinator state. - """ - if flask.request.method == 'GET': return "Test", 200 @@ -182,10 +172,6 @@ def api_node_coordinator_state(node): @api.route('/api/v1/node//domain-state', methods=['GET', 'POST']) @authenticator def api_node_domain_state(node): - """ - Manage NODE domain state. - """ - if flask.request.method == 'GET': return "Test", 200 @@ -343,310 +329,275 @@ def api_vm_node(vm): # # Network endpoints # -@api.route('/api/v1/network', methods=['GET']) +@api.route('/api/v1/network', methods=['GET', 'POST']) @authenticator def api_net(): - """ - Return a list of virtual client networks with limit LIMIT. - """ - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None + if flask.request.method == 'GET': + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + return pvcapi.net_list(limit) - return pvcapi.net_list(limit) + if flask.request.method == 'POST': + # Get network VNI + if 'vni' in flask.request.values: + vni = flask.request.values['vni'] + else: + return flask.jsonify({"message":"ERROR: A VNI must be specified for the virtual network."}), 520 + + # Get network description + if 'description' in flask.request.values: + description = flask.request.values['vni'] + else: + return flask.jsonify({"message":"ERROR: A VNI must be specified for the virtual network."}), 520 + + # Get network type + if 'nettype' in flask.request.values: + nettype = flask.request.values['nettype'] + if not 'managed' in nettype and not 'bridged' in nettype: + return flask.jsonify({"message":"ERROR: A valid nettype must be specified: 'managed' or 'bridged'."}), 520 + else: + return flask.jsonify({"message":"ERROR: A nettype must be specified for the virtual network."}), 520 + + # Get network domain + if 'domain' in flask.request.values: + domain = flask.request.values['domain'] + else: + domain = None + + # Get ipv4 network + if 'ip4_network' in flask.request.values: + ip4_network = flask.request.values['ip4_network'] + else: + ip4_network = None + + # Get ipv4 gateway + if 'ip4_gateway' in flask.request.values: + ip4_gateway = flask.request.values['ip4_gateway'] + else: + ip4_gateway = None + + # Get ipv6 network + if 'ip6_network' in flask.request.values: + ip6_network = flask.request.values['ip6_network'] + else: + ip6_network = None + + # Get ipv6 gateway + if 'ip6_gateway' in flask.request.values: + ip6_gateway = flask.request.values['ip6_gateway'] + else: + ip6_gateway = None + + # Get ipv4 DHCP flag + if 'dhcp4' in flask.request.values and flask.request.values['dhcp4']: + dhcp4_flag = True + else: + dhcp4_flag = False + + # Get ipv4 DHCP start + if 'dhcp4_start' in flask.request.values: + dhcp4_start = flask.request.values['dhcp4_start'] + else: + dhcp4_start = None + + # Get ipv4 DHCP end + if 'dhcp4_end' in flask.request.values: + dhcp4_end = flask.request.values['dhcp4_end'] + else: + dhcp4_end = None + + return pvcapi.net_add(vni, description, nettype, domain, + ip4_network, ip4_gateway, ip6_network, ip6_gateway, + dhcp4_flag, dhcp4_start, dhcp4_end) -@api.route('/api/v1/network/', methods=['GET']) +@api.route('/api/v1/network/', methods=['GET', 'PUT', 'DELETE']) @authenticator -def api_net_info(network): - """ - Get information about a virtual client network with description NETWORK. - """ +def api_net_root(network): # Same as specifying /network?limit=NETWORK - return pvcapi.net_list(network) + if flask.request.method == 'GET': + return pvcapi.net_list(network) -@api.route('/api/v1/network//add', methods=['POST']) + if flask.request.method == 'PUT': + # Get network description + if 'description' in flask.request.values: + description = flask.request.values['description'] + else: + description = None + + # Get network domain + if 'domain' in flask.request.values: + domain = flask.request.values['domain'] + else: + domain = None + + # Get ipv4 network + if 'ip4_network' in flask.request.values: + ip4_network = flask.request.values['ip4_network'] + else: + ip4_network = None + + # Get ipv4 gateway + if 'ip4_gateway' in flask.request.values: + ip4_gateway = flask.request.values['ip4_gateway'] + else: + ip4_gateway = None + + # Get ipv6 network + if 'ip6_network' in flask.request.values: + ip6_network = flask.request.values['ip6_network'] + else: + ip6_network = None + + # Get ipv6 gateway + if 'ip6_gateway' in flask.request.values: + ip6_gateway = flask.request.values['ip6_gateway'] + else: + ip6_gateway = None + + # Get ipv4 DHCP flag + if 'dhcp4' in flask.request.values and flask.request.values['dhcp4']: + dhcp4_flag = True + else: + dhcp4_flag = False + + # Get ipv4 DHCP start + if 'dhcp4_start' in flask.request.values: + dhcp4_start = flask.request.values['dhcp4_start'] + else: + dhcp4_start = None + + # Get ipv4 DHCP end + if 'dhcp4_end' in flask.request.values: + dhcp4_end = flask.request.values['dhcp4_end'] + else: + dhcp4_end = None + + return pvcapi.net_modify(network, description, domain, + ip4_network, ip4_gateway, + ip6_network, ip6_gateway, + dhcp4_flag, dhcp4_start, dhcp4_end) + + if flask.request.method == 'DELETE': + return pvcapi.net_remove(network) + +@api.route('/api/v1/network//lease', methods=['GET', 'POST']) @authenticator -def api_net_add(network): - """ - Add a virtual client network with description NETWORK. - """ - # Get network VNI - if 'vni' in flask.request.values: - vni = flask.request.values['vni'] - else: - return flask.jsonify({"message":"ERROR: A VNI must be specified for the virtual network."}), 520 +def api_net_lease(network): + if flask.request.method == 'GET': + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + # Get static-only flag + if 'static' in flask.request.values and flask.request.values['static']: + flag_static = True + else: + flag_static = False + + return pvcapi.net_dhcp_list(network, limit. flag_static) - # Get network type - if 'nettype' in flask.request.values: - nettype = flask.request.values['nettype'] - if not 'managed' in nettype and not 'bridged' in nettype: - return flask.jsonify({"message":"ERROR: A valid nettype must be specified: 'managed' or 'bridged'."}), 520 - else: - return flask.jsonify({"message":"ERROR: A nettype must be specified for the virtual network."}), 520 + if flask.request.method == 'POST': + # Get lease macaddr + if 'macaddress' in flask.request.values: + macaddress = flask.request.values['macaddress'] + else: + return flask.jsonify({"message":"ERROR: An IP address must be specified for the lease."}), 400 + # Get lease ipaddress + if 'ipaddress' in flask.request.values: + ipaddress = flask.request.values['ipaddress'] + else: + return flask.jsonify({"message":"ERROR: An IP address must be specified for the lease."}), 400 + + # Get lease hostname + if 'hostname' in flask.request.values: + hostname = flask.request.values['hostname'] + else: + hostname = None + + return pvcapi.net_dhcp_add(network, ipaddress, lease, hostname) - # Get network domain - if 'domain' in flask.request.values: - domain = flask.request.values['domain'] - else: - domain = None - - # Get ipv4 network - if 'ip4_network' in flask.request.values: - ip4_network = flask.request.values['ip4_network'] - else: - ip4_network = None - - # Get ipv4 gateway - if 'ip4_gateway' in flask.request.values: - ip4_gateway = flask.request.values['ip4_gateway'] - else: - ip4_gateway = None - - # Get ipv6 network - if 'ip6_network' in flask.request.values: - ip6_network = flask.request.values['ip6_network'] - else: - ip6_network = None - - # Get ipv6 gateway - if 'ip6_gateway' in flask.request.values: - ip6_gateway = flask.request.values['ip6_gateway'] - else: - ip6_gateway = None - - # Get ipv4 DHCP flag - if 'flag_dhcp4' in flask.request.values: - dhcp4_flag = True - else: - dhcp4_flag = False - - # Get ipv4 DHCP start - if 'dhcp4_start' in flask.request.values: - dhcp4_start = flask.request.values['dhcp4_start'] - else: - dhcp4_start = None - - # Get ipv4 DHCP end - if 'dhcp4_end' in flask.request.values: - dhcp4_end = flask.request.values['dhcp4_end'] - else: - dhcp4_end = None - - return pvcapi.net_add(vni, network, nettype, domain, - ip4_network, ip4_gateway, ip6_network, ip6_gateway, - dhcp4_flag, dhcp4_start, dhcp4_end) - -@api.route('/api/v1/network//modify', methods=['POST']) +@api.route('/api/v1/network//lease/', methods=['GET', 'DELETE']) @authenticator -def api_net_modify(network): - """ - Modify a virtual client network with description NETWORK. - """ - # Get network VNI - if 'vni' in flask.request.values: - vni = flask.request.values['vni'] - else: - vni = None +def api_net_lease_root(network, lease): + if flask.request.method == 'GET': + # Same as specifying /network?limit=NETWORK + return pvcapi.net_dhcp_list(network, lease, False) - # Get network type - if 'nettype' in flask.request.values: - nettype = flask.request.values['nettype'] - else: - vni = None + if flask.request.method == 'DELETE': + return pvcapi.net_dhcp_remove(network, lease) - # Get network domain - if 'domain' in flask.request.values: - domain = flask.request.values['domain'] - else: - domain = None - - # Get ipv4 network - if 'ip4_network' in flask.request.values: - ip4_network = flask.request.values['ip4_network'] - else: - ip4_network = None - - # Get ipv4 gateway - if 'ip4_gateway' in flask.request.values: - ip4_gateway = flask.request.values['ip4_gateway'] - else: - ip4_gateway = None - - # Get ipv6 network - if 'ip6_network' in flask.request.values: - ip6_network = flask.request.values['ip6_network'] - else: - ip6_network = None - - # Get ipv6 gateway - if 'ip6_gateway' in flask.request.values: - ip6_gateway = flask.request.values['ip6_gateway'] - else: - ip6_gateway = None - - # Get ipv4 DHCP flag - if 'flag_dhcp4' in flask.request.values: - dhcp4_flag = True - else: - dhcp4_flag = False - - # Get ipv4 DHCP start - if 'dhcp4_start' in flask.request.values: - dhcp4_start = flask.request.values['dhcp4_start'] - else: - dhcp4_start = None - - # Get ipv4 DHCP end - if 'dhcp4_end' in flask.request.values: - dhcp4_end = flask.request.values['dhcp4_end'] - else: - dhcp4_end = None - - return pvcapi.net_modify(vni, network, nettype, domain, - ip4_network, ip4_gateway, ip6_network, ip6_gateway, - dhcp4_flag, dhcp4_start, dhcp4_end) - -@api.route('/api/v1/network//remove', methods=['POST']) -@authenticator -def api_net_remove(network): - """ - Remove a virtual client network with description NETWORK. - """ - return pvcapi.net_remove(network) - -@api.route('/api/v1/network//dhcp', methods=['GET']) -@authenticator -def api_net_dhcp(network): - """ - Return a list of DHCP leases in virtual client network with description NETWORK with limit LIMIT. - """ - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None - - # Get static-only flag - if 'flag_static' in flask.request.values: - flag_static = True - else: - flag_static = False - - return pvcapi.net_dhcp_list(network, limit. flag_static) - -@api.route('/api/v1/network//dhcp/', methods=['GET']) -@authenticator -def api_net_dhcp_info(network, lease): - """ - Get information about a DHCP lease for MAC address LEASE in virtual client network with description NETWORK. - """ - # Same as specifying /network?limit=NETWORK - return pvcapi.net_dhcp_list(network, lease, False) - -@api.route('/api/v1/network//dhcp//add', methods=['POST']) -@authenticator -def api_net_dhcp_add(network, lease): - """ - Add a static DHCP lease for MAC address LEASE to virtual client network with description NETWORK. - """ - # Get lease ipaddress - if 'ipaddress' in flask.request.values: - ipaddress = flask.request.values['ipaddress'] - else: - return flask.jsonify({"message":"ERROR: An IP address must be specified for the lease."}), 520 - - # Get lease hostname - if 'hostname' in flask.request.values: - hostname = flask.request.values['hostname'] - else: - hostname = None - - return pvcapi.net_dhcp_add(network, ipaddress, lease, hostname) - -@api.route('/api/v1/network//dhcp//remove', methods=['POST']) -@authenticator -def api_net_dhcp_remove(network, lease): - """ - Remove a static DHCP lease for MAC address LEASE from virtual client network with description NETWORK. - """ - return pvcapi.net_dhcp_remove(network, lease) - -@api.route('/api/v1/network//acl', methods=['GET']) +@api.route('/api/v1/network//acl', methods=['GET', 'POST']) @authenticator def api_net_acl(network): - """ - Return a list of network ACLs in network NETWORK with limit LIMIT. - """ - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None + if flask.request.method == 'GET': + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + # Get direction limit + if 'direction' in flask.request.values: + direction = flask.request.values['direction'] + if not 'in' in direction and not 'out' in direction: + return flash.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'; for both, do not specify a direction."}), 400 + else: + direction = None + + return pvcapi.net_acl_list(network, limit, direction) - # Get direction limit - if 'direction' in flask.request.values: - direction = flask.request.values['direction'] - if not 'in' in direction and not 'out' in direction: - return flash.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'; for both, do not specify a direction."}), 510 - else: - direction = None + if flask.request.method == 'POST': + # Get ACL description + if 'description' in flask.request.values: + description = flask.request.values['description'] + else: + return flask.jsonify({"message":"ERROR: A description must be provided."}), 400 + + # Get rule direction + if 'direction' in flask.request.values: + direction = flask.request.values['limit'] + if not 'in' in direction and not 'out' in direction: + return flask.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'."}), 400 + else: + return flask.jsonify({"message":"ERROR: A direction must be specified for the ACL."}), 400 + + # Get rule data + if 'rule' in flask.request.values: + rule = flask.request.values['rule'] + else: + return flask.jsonify({"message":"ERROR: A valid NFT rule line must be specified for the ACL."}), 400 + + # Get order value + if 'order' in flask.request.values: + order = flask.request.values['order'] + else: + order = None + + return pvcapi.net_acl_add(network, direction, acl, rule, order) - return pvcapi.net_acl_list(network, limit, direction) - -@api.route('/api/v1/network//acl/', methods=['GET']) +@api.route('/api/v1/network//acl/', methods=['GET', 'DELETE']) @authenticator def api_net_acl_info(network, acl): - """ - Get information about a network access control entry with description ACL in virtual client network with description NETWORK. - """ - # Same as specifying /network?limit=NETWORK - return pvcapi.net_acl_list(network, acl, None) + if flask.request.method == 'GET': + # Same as specifying /network?limit=NETWORK + return pvcapi.net_acl_list(network, acl, None) -@api.route('/api/v1/network//acl//add', methods=['POST']) -@authenticator -def api_net_acl_add(network, acl): - """ - Add an access control list with description ACL to virtual client network with description NETWORK. - """ - # Get rule direction - if 'direction' in flask.request.values: - direction = flask.request.values['limit'] - if not 'in' in direction and not 'out' in direction: - return flask.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'."}), 510 - else: - return flask.jsonify({"message":"ERROR: A direction must be specified for the ACL."}), 510 - - # Get rule data - if 'rule' in flask.request.values: - rule = flask.request.values['rule'] - else: - return flask.jsonify({"message":"ERROR: A valid NFT rule line must be specified for the ACL."}), 510 - - # Get order value - if 'order' in flask.request.values: - order = flask.request.values['order'] - else: - order = None - - return pvcapi.net_acl_add(network, direction, acl, rule, order) - -@api.route('/api/v1/network//acl//remove', methods=['POST']) -@authenticator -def api_net_acl_remove(network, acl): - """ - Remove an access control list with description ACL from virtual client network with description NETWORK. - """ - # Get rule direction - if 'direction' in flask.request.values: - direction = flask.request.values['limit'] - if not 'in' in direction and not 'out' in direction: - return flask.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'."}), 510 - else: - return flask.jsonify({"message":"ERROR: A direction must be specified for the ACL."}), 510 - - return pvcapi.net_acl_remove(network, direction, acl) + if flask.request.method == 'DELETE': + # Get rule direction + if 'direction' in flask.request.values: + direction = flask.request.values['limit'] + if not 'in' in direction and not 'out' in direction: + return flask.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'."}), 400 + else: + return flask.jsonify({"message":"ERROR: A direction must be specified for the ACL."}), 400 + + return pvcapi.net_acl_remove(network, direction, acl) # # Storage (Ceph) endpoints @@ -658,34 +609,22 @@ def api_net_acl_remove(network, acl): # @api.route('/api/v1/storage', methods=['GET']) def api_storage(): - """ - Manage the storage of the PVC cluster. - """ return flask.jsonify({"message":"Manage the storage of the PVC cluster."}), 200 @api.route('/api/v1/storage/ceph', methods=['GET']) @api.route('/api/v1/storage/ceph/status', methods=['GET']) @authenticator def api_ceph_status(): - """ - Get the current Ceph cluster status. - """ return pvcapi.ceph_status() @api.route('/api/v1/storage/ceph/df', methods=['GET']) @authenticator def api_ceph_radosdf(): - """ - Get the current Ceph cluster utilization. - """ return pvcapi.ceph_radosdf() @api.route('/api/v1/storage/ceph/osd', methods=['GET']) @authenticator def api_ceph_osd(): - """ - Get the list of OSDs in the Ceph storage cluster. - """ # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] @@ -697,66 +636,51 @@ def api_ceph_osd(): @api.route('/api/v1/storage/ceph/osd/set', methods=['POST']) @authenticator def api_ceph_osd_set(): - """ - Set OSD option OPTION on the PVC Ceph storage cluster, e.g. 'noout' or 'noscrub'. - """ # Get OSD option if 'option' in flask.request.options: option = flask.request.options['option'] else: - return flask.jsonify({"message":"ERROR: An OSD option must be specified."}), 510 + return flask.jsonify({"message":"ERROR: An OSD option must be specified."}), 400 return pvcapi.ceph_osd_set(option) @api.route('/api/v1/storage/ceph/osd/unset', methods=['POST']) @authenticator def api_ceph_osd_unset(): - """ - Unset OSD option OPTION on the PVC Ceph storage cluster, e.g. 'noout' or 'noscrub'. - """ # Get OSD option if 'option' in flask.request.options: option = flask.request.options['option'] else: - return flask.jsonify({"message":"ERROR: An OSD option must be specified."}), 510 + return flask.jsonify({"message":"ERROR: An OSD option must be specified."}), 400 return pvcapi.ceph_osd_unset(option) @api.route('/api/v1/storage/ceph/osd/', methods=['GET']) @authenticator def api_ceph_osd_info(osd): - """ - Get information about an OSD with ID OSD. - """ # Same as specifying /osd?limit=OSD return pvcapi.ceph_osd_list(osd) @api.route('/api/v1/storage/ceph/osd//add', methods=['POST']) @authenticator def api_ceph_osd_add(node): - """ - Add a Ceph OSD to node NODE. - """ # Get OSD device if 'device' in flask.request.devices: device = flask.request.devices['device'] else: - return flask.jsonify({"message":"ERROR: A block device must be specified."}), 510 + return flask.jsonify({"message":"ERROR: A block device must be specified."}), 400 # Get OSD weight if 'weight' in flask.request.weights: weight = flask.request.weights['weight'] else: - return flask.jsonify({"message":"ERROR: An OSD weight must be specified."}), 510 + return flask.jsonify({"message":"ERROR: An OSD weight must be specified."}), 400 return pvcapi.ceph_osd_add(node, device, weight) @api.route('/api/v1/storage/ceph/osd//remove', methods=['POST']) @authenticator def api_ceph_osd_remove(osd): - """ - Remove a Ceph OSD with ID OSD. - """ # Verify yes-i-really-mean-it flag if not 'flag_yes_i_really_mean_it' in flask.request.values: return flask.jsonify({"message":"ERROR: This command can have unintended consequences and should not be automated; if you're sure you know what you're doing, resend with the argument 'flag_yes_i_really_mean_it'."}), 599 @@ -766,25 +690,16 @@ def api_ceph_osd_remove(osd): @api.route('/api/v1/storage/ceph/osd//in', methods=['POST']) @authenticator def api_ceph_osd_in(osd): - """ - Set in a Ceph OSD with ID OSD. - """ return pvcapi.ceph_osd_in(osd) @api.route('/api/v1/storage/ceph/osd//out', methods=['POST']) @authenticator def api_ceph_osd_out(osd): - """ - Set out a Ceph OSD with ID OSD. - """ return pvcapi.ceph_osd_out(osd) @api.route('/api/v1/storage/ceph/pool', methods=['GET']) @authenticator def api_ceph_pool(): - """ - Get the list of RBD pools in the Ceph storage cluster. - """ # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] @@ -796,18 +711,12 @@ def api_ceph_pool(): @api.route('/api/v1/storage/ceph/pool/', methods=['GET']) @authenticator def api_ceph_pool_info(pool): - """ - Get information about an RBD pool with name POOL. - """ # Same as specifying /pool?limit=POOL return pvcapi.ceph_pool_list(pool) @api.route('/api/v1/storage/ceph/pool//add', methods=['POST']) @authenticator def api_ceph_pool_add(pool): - """ - Add a Ceph RBD pool with name POOL. - """ # Get placement groups if 'pgs' in flask.request.values: pgs = flask.request.values['pgs'] @@ -820,9 +729,6 @@ def api_ceph_pool_add(pool): @api.route('/api/v1/storage/ceph/pool//remove', methods=['POST']) @authenticator def api_ceph_pool_remove(pool): - """ - Remove a Ceph RBD pool with name POOL. - """ # Verify yes-i-really-mean-it flag if not 'flag_yes_i_really_mean_it' in flask.request.values: return flask.jsonify({"message":"ERROR: This command can have unintended consequences and should not be automated; if you're sure you know what you're doing, resend with the argument 'flag_yes_i_really_mean_it'."}), 599 @@ -832,9 +738,6 @@ def api_ceph_pool_remove(pool): @api.route('/api/v1/storage/ceph/volume', methods=['GET']) @authenticator def api_ceph_volume(): - """ - Get the list of RBD volumes in the Ceph storage cluster. - """ # Get pool limit if 'pool' in flask.request.values: pool = flask.request.values['pool'] @@ -852,40 +755,28 @@ def api_ceph_volume(): @api.route('/api/v1/storage/ceph/volume//', methods=['GET']) @authenticator def api_ceph_volume_info(pool, volume): - """ - Get information about an RBD volume with name VOLUME in RBD pool with name POOL. - """ # Same as specifying /volume?limit=VOLUME return pvcapi.ceph_osd_list(pool, osd) @api.route('/api/v1/storage/ceph/volume///add', methods=['POST']) @authenticator def api_ceph_volume_add(pool, volume): - """ - Add a Ceph RBD volume with name VOLUME to RBD pool with name POOL. - """ # Get volume size if 'size' in flask.request.values: size = flask.request.values['size'] else: - return flask.jsonify({"message":"ERROR: A volume size in bytes (or with an M/G/T suffix) must be specified."}), 510 + return flask.jsonify({"message":"ERROR: A volume size in bytes (or with an M/G/T suffix) must be specified."}), 400 return pvcapi.ceph_volume_add(pool, volume, size) @api.route('/api/v1/storage/ceph/volume///remove', methods=['POST']) @authenticator def api_ceph_volume_remove(pool, volume): - """ - Remove a Ceph RBD volume with name VOLUME from RBD pool with name POOL. - """ return pvcapi.ceph_volume_remove(pool, volume) @api.route('/api/v1/storage/ceph/volume/snapshot', methods=['GET']) @authenticator def api_ceph_volume_snapshot(): - """ - Get the list of RBD volume snapshots in the Ceph storage cluster. - """ # Get pool limit if 'pool' in flask.request.values: pool = flask.request.values['pool'] @@ -909,26 +800,17 @@ def api_ceph_volume_snapshot(): @api.route('/api/v1/storage/ceph/volume/snapshot///', methods=['GET']) @authenticator def api_ceph_volume_snapshot_info(pool, volume, snapshot): - """ - Get information about a snapshot with name SNAPSHOT of RBD volume with name VOLUME in RBD pool with name POOL. - """ # Same as specifying /snapshot?limit=VOLUME return pvcapi.ceph_snapshot_list(pool, volume, snapshot) @api.route('/api/v1/storage/ceph/volume/snapshot////add', methods=['POST']) @authenticator def api_ceph_volume_snapshot_add(pool, volume, snapshot): - """ - Add a Ceph RBD volume snapshot with name SNAPSHOT of RBD volume with name VOLUME in RBD pool with name POOL. - """ return pvcapi.ceph_volume_snapshot_add(pool, volume, snapshot) @api.route('/api/v1/storage/ceph/volume/snapshot////remove', methods=['POST']) @authenticator def api_ceph_volume_snapshot_remove(pool, volume, snapshot): - """ - Remove a Ceph RBD volume snapshot with name SNAPSHOT from RBD volume with name VOLUME in RBD pool with name POOL. - """ return pvcapi.ceph_volume_snapshot_remove(pool, volume, snapshot) # diff --git a/docs/manuals/api.md b/docs/manuals/api.md index dd690879..d5099173 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -277,21 +277,19 @@ Add a new virtual network to the cluster. `vni` must be a valid VNI, either a vL * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about the virtual network with description ``. The output is identical to `/api/v1/network?limit=` without fuzzy regex matching. +Return a JSON document containing information about the virtual network ``. The output is identical to `/api/v1/network?limit=` without fuzzy regex matching. ###### `PUT` * Mandatory values: N/A - * Optional values: `vni`, `nettype` `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `dhcp4`, `dhcp4_start`, `dhcp4_end` + * Optional values: `domain`, `ip4_network`, `ip4_gateway`, `ip6_network`, `ip6_gateway`, `dhcp4`, `dhcp4_start`, `dhcp4_end` -Modify the options of an existing virtual network with description ``. +Modify the options of an existing virtual network ``. All values are optional and are identical to the values for `add`. Only those values specified will be updated. -**NOTE:** Changing the `vni` or `nettype` of a virtual network is technically possible, but is not recommended. This would require updating all VMs in the network. It is usually advisable to create a new virtual network with the new VNI and type, move VMs to it, then finally remove the old virtual network. - ###### `DELETE` -Remove a virtual network with description ``. +Remove a virtual network ``. #### `/api/v1/network//lease` * Methods: `GET`, `POST` @@ -300,7 +298,7 @@ Remove a virtual network with description ``. * Mandatory values: N/A * Optional values: `limit`, `static` -Return a JSON document containing information about all active DHCP leases in virtual network with description ``. +Return a JSON document containing information about all active DHCP leases in virtual network ``. If `limit` is specified, return a JSON document containing information about all active DHCP leases with MAC addresses matching `limit` as fuzzy regex. @@ -310,7 +308,7 @@ If `static` is specified, only return static DHCP leases. * Mandatory values: `macaddress`, `ipaddress` * Optional values: `hostname` -Add a new static DHCP lease for MAC address `` in virtual network with description ``. +Add a new static DHCP lease for MAC address `` in virtual network ``. `ipaddress` must be a valid IP address in the specified `` IPv4 netblock, and ideally outside of the DHCPv4 range. @@ -323,13 +321,13 @@ Add a new static DHCP lease for MAC address `` in virtual network wi * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about DHCP lease with MAC address `` in virtual network with description ``. The output is identical to `/api/v1/network//dhcp?limit=` without fuzzy regex matching. +Return a JSON document containing information about DHCP lease with MAC address `` in virtual network ``. The output is identical to `/api/v1/network//dhcp?limit=` without fuzzy regex matching. ###### `DELETE` * Mandatory values: N/A * Optional values: N/A -Remove a static DHCP lease for MAC address ` in virtual network with description ``. +Remove a static DHCP lease for MAC address ` in virtual network ``. #### `/api/v1/network//acl` * Methods: `GET`, `POST` @@ -338,17 +336,17 @@ Remove a static DHCP lease for MAC address ` in virtual network with des * Mandatory values: N/A * Optional values: `limit`, `direction` -Return a JSON document containing information about all active NFTables ACLs in virtual network with description ``. +Return a JSON document containing information about all active NFTables ACLs in virtual network ``. If `limit` is specified, return a JSON document containing information about all active NFTables ACLs with descriptions matching `limit` as fuzzy regex. If `direction` is specified and is one of `in` or `out`, return a JSON codument listing all active NFTables ACLs in the specified direction only. If `direction` is invalid, return a failure. ###### `POST` - * Mandatory values: `direction`, `rule` + * Mandatory values: `description`, `direction`, `rule` * Optional values: `order` -Add a new NFTables ACL with description `` in virtual network with description ``. +Add a new NFTables ACL with `description` in virtual network ``. `direction` must be one of `in` or `out`. @@ -363,15 +361,15 @@ Add a new NFTables ACL with description `` in virtual network with descript * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about NFTables ACL with description `` in virtual network with description ``. The output is identical to `/api/v1/network//acl?limit=` without fuzzy regex matching. +Return a JSON document containing information about NFTables ACL with description `` in virtual network ``. The output is identical to `/api/v1/network//acl?limit=` without fuzzy regex matching. If `` is not valid, return an empty JSON document. ###### `DELETE` - * Mandatory values: N/A + * Mandatory values: `direction` * Optional values: N/A -Remove an NFTables ACL with description `` from virtual network with description ``. +Remove an NFTables ACL with description `` in direction `direction` from virtual network ``. ### Storage (Ceph) endpoints From 4cf3ade10a3e03fe2845155027663518ab491511 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 26 Jul 2019 11:00:11 -0400 Subject: [PATCH 13/14] RESTify the remaining components --- client-api/pvc-api.py | 479 +++++++++++++++++++++++------------------- docs/manuals/api.md | 30 +-- 2 files changed, 278 insertions(+), 231 deletions(-) diff --git a/client-api/pvc-api.py b/client-api/pvc-api.py index 45408437..3e23913f 100755 --- a/client-api/pvc-api.py +++ b/client-api/pvc-api.py @@ -137,7 +137,7 @@ def api_auth_logout(): # @api.route('/api/v1/node', methods=['GET']) @authenticator -def api_node(): +def api_node_root(): # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] @@ -148,7 +148,7 @@ def api_node(): @api.route('/api/v1/node/', methods=['GET']) @authenticator -def api_node_root(node): +def api_node_element(node): # Same as specifying /node?limit=NODE return pvcapi.node_list(node) @@ -190,26 +190,26 @@ def api_node_domain_state(node): # @api.route('/api/v1/vm', methods=['GET', 'POST']) @authenticator -def api_vm(): +def api_vm_root(): if flask.request.method == 'GET': # Get node limit if 'node' in flask.request.values: node = flask.request.values['node'] else: node = None - + # Get state limit if 'state' in flask.request.values: state = flask.request.values['state'] else: state = None - + # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] else: limit = None - + return pvcapi.vm_list(node, state, limit) if flask.request.method == 'POST': @@ -218,24 +218,24 @@ def api_vm(): libvirt_xml = flask.request.values['xml'] else: return flask.jsonify({"message":"ERROR: A Libvirt XML document must be specified."}), 400 - + # Get node name if 'node' in flask.request.values: node = flask.request.values['node'] else: node = None - + # Get target selector if 'selector' in flask.request.values: selector = flask.request.values['selector'] else: selector = None - + return pvcapi.vm_define(vm, libvirt_xml, node, selector) @api.route('/api/v1/vm/', methods=['GET', 'POST', 'PUT', 'DELETE']) @authenticator -def api_vm_root(vm): +def api_vm_element(vm): if flask.request.method == 'GET': # Same as specifying /vm?limit=VM return pvcapi.vm_list(None, None, vm, is_fuzzy=False) @@ -246,12 +246,12 @@ def api_vm_root(vm): if flask.request.method == 'PUT': libvirt_xml = flask.request.data - + if 'restart' in flask.request.values and flask.request.values['restart']: flag_restart = True else: flag_restart = False - + return pvcapi.vm_modify(vm, flag_restart, libvirt_xml) if flask.request.method == 'DELETE': @@ -285,13 +285,13 @@ def api_vm_state(vm): def api_vm_node(vm): if flask.request.method == 'GET': return "Test", 200 - + if flask.request.method == 'POST': if 'action' in flask.request.values: action = flask.request.values['action'] else: flask.abort(400) - + # Get node name if 'node' in flask.request.values: node = flask.request.values['node'] @@ -331,14 +331,14 @@ def api_vm_node(vm): # @api.route('/api/v1/network', methods=['GET', 'POST']) @authenticator -def api_net(): +def api_net_root(): if flask.request.method == 'GET': # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] else: limit = None - + return pvcapi.net_list(limit) if flask.request.method == 'POST': @@ -347,13 +347,13 @@ def api_net(): vni = flask.request.values['vni'] else: return flask.jsonify({"message":"ERROR: A VNI must be specified for the virtual network."}), 520 - + # Get network description if 'description' in flask.request.values: description = flask.request.values['vni'] else: return flask.jsonify({"message":"ERROR: A VNI must be specified for the virtual network."}), 520 - + # Get network type if 'nettype' in flask.request.values: nettype = flask.request.values['nettype'] @@ -361,62 +361,62 @@ def api_net(): return flask.jsonify({"message":"ERROR: A valid nettype must be specified: 'managed' or 'bridged'."}), 520 else: return flask.jsonify({"message":"ERROR: A nettype must be specified for the virtual network."}), 520 - + # Get network domain if 'domain' in flask.request.values: domain = flask.request.values['domain'] else: domain = None - + # Get ipv4 network if 'ip4_network' in flask.request.values: ip4_network = flask.request.values['ip4_network'] else: ip4_network = None - + # Get ipv4 gateway if 'ip4_gateway' in flask.request.values: ip4_gateway = flask.request.values['ip4_gateway'] else: ip4_gateway = None - + # Get ipv6 network if 'ip6_network' in flask.request.values: ip6_network = flask.request.values['ip6_network'] else: ip6_network = None - + # Get ipv6 gateway if 'ip6_gateway' in flask.request.values: ip6_gateway = flask.request.values['ip6_gateway'] else: ip6_gateway = None - + # Get ipv4 DHCP flag if 'dhcp4' in flask.request.values and flask.request.values['dhcp4']: dhcp4_flag = True else: dhcp4_flag = False - + # Get ipv4 DHCP start if 'dhcp4_start' in flask.request.values: dhcp4_start = flask.request.values['dhcp4_start'] else: dhcp4_start = None - + # Get ipv4 DHCP end if 'dhcp4_end' in flask.request.values: dhcp4_end = flask.request.values['dhcp4_end'] else: dhcp4_end = None - + return pvcapi.net_add(vni, description, nettype, domain, ip4_network, ip4_gateway, ip6_network, ip6_gateway, dhcp4_flag, dhcp4_start, dhcp4_end) @api.route('/api/v1/network/', methods=['GET', 'PUT', 'DELETE']) @authenticator -def api_net_root(network): +def api_net_element(network): # Same as specifying /network?limit=NETWORK if flask.request.method == 'GET': return pvcapi.net_list(network) @@ -427,55 +427,55 @@ def api_net_root(network): description = flask.request.values['description'] else: description = None - + # Get network domain if 'domain' in flask.request.values: domain = flask.request.values['domain'] else: domain = None - + # Get ipv4 network if 'ip4_network' in flask.request.values: ip4_network = flask.request.values['ip4_network'] else: ip4_network = None - + # Get ipv4 gateway if 'ip4_gateway' in flask.request.values: ip4_gateway = flask.request.values['ip4_gateway'] else: ip4_gateway = None - + # Get ipv6 network if 'ip6_network' in flask.request.values: ip6_network = flask.request.values['ip6_network'] else: ip6_network = None - + # Get ipv6 gateway if 'ip6_gateway' in flask.request.values: ip6_gateway = flask.request.values['ip6_gateway'] else: ip6_gateway = None - + # Get ipv4 DHCP flag if 'dhcp4' in flask.request.values and flask.request.values['dhcp4']: dhcp4_flag = True else: dhcp4_flag = False - + # Get ipv4 DHCP start if 'dhcp4_start' in flask.request.values: dhcp4_start = flask.request.values['dhcp4_start'] else: dhcp4_start = None - + # Get ipv4 DHCP end if 'dhcp4_end' in flask.request.values: dhcp4_end = flask.request.values['dhcp4_end'] else: dhcp4_end = None - + return pvcapi.net_modify(network, description, domain, ip4_network, ip4_gateway, ip6_network, ip6_gateway, @@ -486,20 +486,20 @@ def api_net_root(network): @api.route('/api/v1/network//lease', methods=['GET', 'POST']) @authenticator -def api_net_lease(network): +def api_net_lease_root(network): if flask.request.method == 'GET': # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] else: limit = None - + # Get static-only flag if 'static' in flask.request.values and flask.request.values['static']: flag_static = True else: flag_static = False - + return pvcapi.net_dhcp_list(network, limit. flag_static) if flask.request.method == 'POST': @@ -513,18 +513,18 @@ def api_net_lease(network): ipaddress = flask.request.values['ipaddress'] else: return flask.jsonify({"message":"ERROR: An IP address must be specified for the lease."}), 400 - + # Get lease hostname if 'hostname' in flask.request.values: hostname = flask.request.values['hostname'] else: hostname = None - + return pvcapi.net_dhcp_add(network, ipaddress, lease, hostname) @api.route('/api/v1/network//lease/', methods=['GET', 'DELETE']) @authenticator -def api_net_lease_root(network, lease): +def api_net_lease_element(network, lease): if flask.request.method == 'GET': # Same as specifying /network?limit=NETWORK return pvcapi.net_dhcp_list(network, lease, False) @@ -534,14 +534,14 @@ def api_net_lease_root(network, lease): @api.route('/api/v1/network//acl', methods=['GET', 'POST']) @authenticator -def api_net_acl(network): +def api_net_acl_root(network): if flask.request.method == 'GET': # Get name limit if 'limit' in flask.request.values: limit = flask.request.values['limit'] else: limit = None - + # Get direction limit if 'direction' in flask.request.values: direction = flask.request.values['direction'] @@ -549,7 +549,7 @@ def api_net_acl(network): return flash.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'; for both, do not specify a direction."}), 400 else: direction = None - + return pvcapi.net_acl_list(network, limit, direction) if flask.request.method == 'POST': @@ -558,7 +558,7 @@ def api_net_acl(network): description = flask.request.values['description'] else: return flask.jsonify({"message":"ERROR: A description must be provided."}), 400 - + # Get rule direction if 'direction' in flask.request.values: direction = flask.request.values['limit'] @@ -566,24 +566,24 @@ def api_net_acl(network): return flask.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'."}), 400 else: return flask.jsonify({"message":"ERROR: A direction must be specified for the ACL."}), 400 - + # Get rule data if 'rule' in flask.request.values: rule = flask.request.values['rule'] else: return flask.jsonify({"message":"ERROR: A valid NFT rule line must be specified for the ACL."}), 400 - + # Get order value if 'order' in flask.request.values: order = flask.request.values['order'] else: order = None - + return pvcapi.net_acl_add(network, direction, acl, rule, order) @api.route('/api/v1/network//acl/', methods=['GET', 'DELETE']) @authenticator -def api_net_acl_info(network, acl): +def api_net_acl_element(network, acl): if flask.request.method == 'GET': # Same as specifying /network?limit=NETWORK return pvcapi.net_acl_list(network, acl, None) @@ -596,7 +596,7 @@ def api_net_acl_info(network, acl): return flask.jsonify({"message":"ERROR: Direction must be either 'in' or 'out'."}), 400 else: return flask.jsonify({"message":"ERROR: A direction must be specified for the ACL."}), 400 - + return pvcapi.net_acl_remove(network, direction, acl) # @@ -622,196 +622,243 @@ def api_ceph_status(): def api_ceph_radosdf(): return pvcapi.ceph_radosdf() -@api.route('/api/v1/storage/ceph/osd', methods=['GET']) +@api.route('/api/v1/storage/ceph/cluster-option', methods=['GET', 'POST']) @authenticator -def api_ceph_osd(): - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None +def api_ceph_cluster_option(): + if flask.request.method == 'GET': + pass - return pvcapi.ceph_osd_list(limit) + if flask.request.method == 'POST': + # Get action + if 'action' in flask.request.values: + action = flask.request.values['action'] + if not 'set' in action and not 'unset' in action: + return flask.jsonify({"message":"ERROR: Action must be one of: set, unset"}), 400 + else: + return flask.jsonify({"message":"ERROR: An action must be specified."}), 400 + # Get option + if 'option' in flask.request.values: + option = flask.request.values['option'] + else: + return flask.jsonify({"message":"ERROR: An option must be specified."}), 400 -@api.route('/api/v1/storage/ceph/osd/set', methods=['POST']) + if action == 'set': + return pvcapi.ceph_osd_set(option) + if action == 'unset': + return pvcapi.ceph_osd_unset(option) + +@api.route('/api/v1/storage/ceph/osd', methods=['GET', 'POST']) @authenticator -def api_ceph_osd_set(): - # Get OSD option - if 'option' in flask.request.options: - option = flask.request.options['option'] - else: - return flask.jsonify({"message":"ERROR: An OSD option must be specified."}), 400 +def api_ceph_osd_root(): + if flask.request.method == 'GET': + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None - return pvcapi.ceph_osd_set(option) + return pvcapi.ceph_osd_list(limit) -@api.route('/api/v1/storage/ceph/osd/unset', methods=['POST']) + if flask.request.method == 'POST': + # Get OSD node + if 'node' in flask.request.values: + node = flask.request.values['node'] + else: + return flask.jsonify({"message":"ERROR: A node must be specified."}), 400 + + # Get OSD device + if 'device' in flask.request.values: + device = flask.request.values['device'] + else: + return flask.jsonify({"message":"ERROR: A block device must be specified."}), 400 + + # Get OSD weight + if 'weight' in flask.request.values: + weight = flask.request.values['weight'] + else: + return flask.jsonify({"message":"ERROR: An OSD weight must be specified."}), 400 + + return pvcapi.ceph_osd_add(node, device, weight) + +@api.route('/api/v1/storage/ceph/osd/', methods=['GET', 'DELETE']) @authenticator -def api_ceph_osd_unset(): - # Get OSD option - if 'option' in flask.request.options: - option = flask.request.options['option'] - else: - return flask.jsonify({"message":"ERROR: An OSD option must be specified."}), 400 +def api_ceph_osd_element(osd): + if flask.request.method == 'GET': + # Same as specifying /osd?limit=OSD + return pvcapi.ceph_osd_list(osd) - return pvcapi.ceph_osd_unset(option) + if flask.request.method == 'DELETE': + # Verify yes-i-really-mean-it flag + if not 'yes_i_really_mean_it' in flask.request.values: + return flask.jsonify({"message":"ERROR: This command can have unintended consequences and should not be automated; if you're sure you know what you're doing, resend with the argument 'yes_i_really_mean_it'."}), 400 -@api.route('/api/v1/storage/ceph/osd/', methods=['GET']) + return pvcapi.ceph_osd_remove(osd) + +@api.route('/api/v1/storage/ceph/osd//state', methods=['GET', 'POST']) @authenticator -def api_ceph_osd_info(osd): - # Same as specifying /osd?limit=OSD - return pvcapi.ceph_osd_list(osd) +def api_ceph_osd_state(osd): + if flask.request.method == 'GET': + pass -@api.route('/api/v1/storage/ceph/osd//add', methods=['POST']) + if flask.request.method == 'POST': + if 'state' in flask.request.values: + state = flask.request.values['state'] + if not 'in' in state and not 'out' in state: + return flask.jsonify({"message":"ERROR: State must be one of: in, out."}), 400 + else: + return flask.jsonify({"message":"ERROR: A state must be specified."}), 400 + + if state == 'in': + return pvcapi.ceph_osd_in(osd) + if state == 'out': + return pvcapi.ceph_osd_out(osd) + +@api.route('/api/v1/storage/ceph/pool', methods=['GET', 'POST']) @authenticator -def api_ceph_osd_add(node): - # Get OSD device - if 'device' in flask.request.devices: - device = flask.request.devices['device'] - else: - return flask.jsonify({"message":"ERROR: A block device must be specified."}), 400 +def api_ceph_pool_root(): + if flask.request.method == 'GET': + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + return pvcapi.ceph_pool_list(limit) - # Get OSD weight - if 'weight' in flask.request.weights: - weight = flask.request.weights['weight'] - else: - return flask.jsonify({"message":"ERROR: An OSD weight must be specified."}), 400 + if flask.request.method == 'POST': + # Get pool name + if 'pool' in flask.request.values: + pool = flask.request.values['pool'] + else: + return flask.jsonify({"message":"ERROR: A pool name must be specified."}), 400 + + # Get placement groups + if 'pgs' in flask.request.values: + pgs = flask.request.values['pgs'] + else: + # We default to a very small number; DOCUMENT THIS + pgs = 128 + + return pvcapi.ceph_pool_add(pool, pgs) - return pvcapi.ceph_osd_add(node, device, weight) - -@api.route('/api/v1/storage/ceph/osd//remove', methods=['POST']) +@api.route('/api/v1/storage/ceph/pool/', methods=['GET', 'DELETE']) @authenticator -def api_ceph_osd_remove(osd): - # Verify yes-i-really-mean-it flag - if not 'flag_yes_i_really_mean_it' in flask.request.values: - return flask.jsonify({"message":"ERROR: This command can have unintended consequences and should not be automated; if you're sure you know what you're doing, resend with the argument 'flag_yes_i_really_mean_it'."}), 599 +def api_ceph_pool_element(pool): + if flask.request.method == 'GET': + # Same as specifying /pool?limit=POOL + return pvcapi.ceph_pool_list(pool) - return pvcapi.ceph_osd_remove(osd) + if flask.request.method == 'DELETE': + # Verify yes-i-really-mean-it flag + if not 'yes_i_really_mean_it' in flask.request.values: + return flask.jsonify({"message":"ERROR: This command can have unintended consequences and should not be automated; if you're sure you know what you're doing, resend with the argument 'yes_i_really_mean_it'."}), 400 + + return pvcapi.ceph_pool_remove(pool) -@api.route('/api/v1/storage/ceph/osd//in', methods=['POST']) +@api.route('/api/v1/storage/ceph/volume', methods=['GET', 'POST']) @authenticator -def api_ceph_osd_in(osd): - return pvcapi.ceph_osd_in(osd) +def api_ceph_volume_root(): + if flask.request.method == 'GET': + # Get pool limit + if 'pool' in flask.request.values: + pool = flask.request.values['pool'] + else: + pool = None + + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + return pvcapi.ceph_volume_list(pool, limit) -@api.route('/api/v1/storage/ceph/osd//out', methods=['POST']) + if flask.request.method == 'POST': + # Get volume name + if 'volume' in flask.request.values: + volume = flask.request.values['volume'] + else: + return flask.jsonify({"message":"ERROR: A volume name must be specified."}), 400 + + # Get volume pool + if 'pool' in flask.request.values: + pool = flask.request.values['pool'] + else: + return flask.jsonify({"message":"ERROR: A pool name must be spcified."}), 400 + + # Get volume size + if 'size' in flask.request.values: + size = flask.request.values['size'] + else: + return flask.jsonify({"message":"ERROR: A volume size in bytes (or with an M/G/T suffix) must be specified."}), 400 + + return pvcapi.ceph_volume_add(pool, volume, size) + +@api.route('/api/v1/storage/ceph/volume//', methods=['GET', 'DELETE']) @authenticator -def api_ceph_osd_out(osd): - return pvcapi.ceph_osd_out(osd) +def api_ceph_volume_element(pool, volume): + if flask.request.method == 'GET': + # Same as specifying /volume?limit=VOLUME + return pvcapi.ceph_volume_list(pool, volume) -@api.route('/api/v1/storage/ceph/pool', methods=['GET']) + if flask.request.method == 'DELETE': + return pvcapi.ceph_volume_remove(pool, volume) + +@api.route('/api/v1/storage/ceph/volume/snapshot', methods=['GET', 'POST']) @authenticator -def api_ceph_pool(): - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None +def api_ceph_volume_snapshot_root(): + if flask.request.method == 'GET': + # Get pool limit + if 'pool' in flask.request.values: + pool = flask.request.values['pool'] + else: + pool = None + + # Get volume limit + if 'volume' in flask.request.values: + volume = flask.request.values['volume'] + else: + volume = None + + # Get name limit + if 'limit' in flask.request.values: + limit = flask.request.values['limit'] + else: + limit = None + + return pvcapi.ceph_volume_snapshot_list(pool, volume, limit) - return pvcapi.ceph_pool_list(limit) + if flask.request.method == 'POST': + # Get snapshot name + if 'snapshot' in flask.request.values: + snapshot = flask.request.values['snapshot'] + else: + return flask.jsonify({"message":"ERROR: A snapshot name must be specified."}), 400 + + # Get volume name + if 'volume' in flask.request.values: + volume = flask.request.values['volume'] + else: + return flask.jsonify({"message":"ERROR: A volume name must be specified."}), 400 + + # Get volume pool + if 'pool' in flask.request.values: + pool = flask.request.values['pool'] + else: + return flask.jsonify({"message":"ERROR: A pool name must be spcified."}), 400 + + return pvcapi.ceph_volume_snapshot_add(pool, volume, snapshot) -@api.route('/api/v1/storage/ceph/pool/', methods=['GET']) + +@api.route('/api/v1/storage/ceph/volume/snapshot///', methods=['GET', 'DELETE']) @authenticator -def api_ceph_pool_info(pool): - # Same as specifying /pool?limit=POOL - return pvcapi.ceph_pool_list(pool) +def api_ceph_volume_snapshot_element(pool, volume, snapshot): + if flask.request.method == 'GET': + # Same as specifying /snapshot?limit=VOLUME + return pvcapi.ceph_volume_snapshot_list(pool, volume, snapshot) -@api.route('/api/v1/storage/ceph/pool//add', methods=['POST']) -@authenticator -def api_ceph_pool_add(pool): - # Get placement groups - if 'pgs' in flask.request.values: - pgs = flask.request.values['pgs'] - else: - # We default to a very small number; DOCUMENT THIS - pgs = 128 - - return pvcapi.ceph_pool_add(pool, pgs) - -@api.route('/api/v1/storage/ceph/pool//remove', methods=['POST']) -@authenticator -def api_ceph_pool_remove(pool): - # Verify yes-i-really-mean-it flag - if not 'flag_yes_i_really_mean_it' in flask.request.values: - return flask.jsonify({"message":"ERROR: This command can have unintended consequences and should not be automated; if you're sure you know what you're doing, resend with the argument 'flag_yes_i_really_mean_it'."}), 599 - - return pvcapi.ceph_pool_remove(pool) - -@api.route('/api/v1/storage/ceph/volume', methods=['GET']) -@authenticator -def api_ceph_volume(): - # Get pool limit - if 'pool' in flask.request.values: - pool = flask.request.values['pool'] - else: - pool = None - - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None - - return pvcapi.ceph_volume_list(pool, limit) - -@api.route('/api/v1/storage/ceph/volume//', methods=['GET']) -@authenticator -def api_ceph_volume_info(pool, volume): - # Same as specifying /volume?limit=VOLUME - return pvcapi.ceph_osd_list(pool, osd) - -@api.route('/api/v1/storage/ceph/volume///add', methods=['POST']) -@authenticator -def api_ceph_volume_add(pool, volume): - # Get volume size - if 'size' in flask.request.values: - size = flask.request.values['size'] - else: - return flask.jsonify({"message":"ERROR: A volume size in bytes (or with an M/G/T suffix) must be specified."}), 400 - - return pvcapi.ceph_volume_add(pool, volume, size) - -@api.route('/api/v1/storage/ceph/volume///remove', methods=['POST']) -@authenticator -def api_ceph_volume_remove(pool, volume): - return pvcapi.ceph_volume_remove(pool, volume) - -@api.route('/api/v1/storage/ceph/volume/snapshot', methods=['GET']) -@authenticator -def api_ceph_volume_snapshot(): - # Get pool limit - if 'pool' in flask.request.values: - pool = flask.request.values['pool'] - else: - pool = None - - # Get volume limit - if 'volume' in flask.request.values: - volume = flask.request.values['volume'] - else: - volume = None - - # Get name limit - if 'limit' in flask.request.values: - limit = flask.request.values['limit'] - else: - limit = None - - return pvcapi.ceph_volume_snapshot_list(pool, volume, limit) - -@api.route('/api/v1/storage/ceph/volume/snapshot///', methods=['GET']) -@authenticator -def api_ceph_volume_snapshot_info(pool, volume, snapshot): - # Same as specifying /snapshot?limit=VOLUME - return pvcapi.ceph_snapshot_list(pool, volume, snapshot) - -@api.route('/api/v1/storage/ceph/volume/snapshot////add', methods=['POST']) -@authenticator -def api_ceph_volume_snapshot_add(pool, volume, snapshot): - return pvcapi.ceph_volume_snapshot_add(pool, volume, snapshot) - -@api.route('/api/v1/storage/ceph/volume/snapshot////remove', methods=['POST']) -@authenticator -def api_ceph_volume_snapshot_remove(pool, volume, snapshot): - return pvcapi.ceph_volume_snapshot_remove(pool, volume, snapshot) + if flask.request.method == 'DELETE': + return pvcapi.ceph_volume_snapshot_remove(pool, volume, snapshot) # # Entrypoint diff --git a/docs/manuals/api.md b/docs/manuals/api.md index d5099173..bfef60a2 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -399,7 +399,7 @@ This endpoint is an alias for `/api/v1/storage/ceph`. Return a JSON document containing information about the current Ceph cluster utilization. The JSON element `ceph_data` contains the raw output of a `rados df` command. #### `/api/v1/storage/ceph/cluster-option` - * Methods: `POST` + * Methods: `GET`, `POST` * Mandatory values: `action`, `option` * Optional values: N/A @@ -418,7 +418,7 @@ Return a JSON document containing information about all Ceph OSDs in the storage * Mandatory values: `node`, `device`, `weight` * Optional values: N/A -Add a new Ceph OSD to PVC node with name ``. `device` must be a valid block device on the specified ``, e.g. `/dev/sdb`. `weight` must be a valid Ceph OSD weight, usually `1.0` if all OSD disks are the same size. +Add a new Ceph OSD to PVC node ``. `device` must be a valid block device on the specified ``, e.g. `/dev/sdb`. `weight` must be a valid Ceph OSD weight, usually `1.0` if all OSD disks are the same size. #### `/api/v1/storage/ceph/osd/` * Methods: `GET`, `DELETE` @@ -467,7 +467,7 @@ Return a JSON document containing information about all Ceph RBD pools in the st * Mandatory values: `pool`, `pgs` * Optional values: N/A -Add a new Ceph RBD pool with name `` to the storage cluster. `pgs` must be a valid number of Placement Groups for the pool, taking into account the number of OSDs and the replication of the pool (`copies=3`). `256` is a safe and sane number of PGs for 3 nodes and 2 disks per node. This value can be grown later via `ceph` commands as required. +Add a new Ceph RBD pool `` to the storage cluster. `pgs` must be a valid number of Placement Groups for the pool, taking into account the number of OSDs and the replication of the pool (`copies=3`). `256` is a safe and sane number of PGs for 3 nodes and 2 disks per node. This value can be grown later via `ceph` commands as required. #### `/api/v1/storage/ceph/pool/` * Methods: `GET`, `DELETE` @@ -476,13 +476,13 @@ Add a new Ceph RBD pool with name `` to the storage cluster. `pgs` must be * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/pool?limit=` without fuzzy regex matching. +Return a JSON document containing information about Ceph RBD pool ``. The output is identical to `/api/v1/storage/ceph/pool?limit=` without fuzzy regex matching. ###### `DELETE` * Mandatory values: `yes_i_really_mean_it` * Optional values: N/A -Remove a Ceph RBD pool with name `` from the storage cluster. +Remove a Ceph RBD pool `` from the storage cluster. **NOTE:** This is a command with potentially dangerous unintended consequences that should not be scripted. To acknowledge the danger, the `yes_i_really_mean_it` must be set or the endpoint will return a failure. @@ -495,13 +495,13 @@ Remove a Ceph RBD pool with name `` from the storage cluster. * Mandatory values: N/A * Optional values: `pool`, `limit` -Return a JSON document containing information about all Ceph RBD volumes in the storage cluster. If `pool` is specified, return a JSON document containing information about all Ceph RBD volumes in Ceph RBD pool with name `pool`. If `limit` is specified, return a JSON document containing information about all Ceph RBD volumes with names matching `limit` as fuzzy regex. +Return a JSON document containing information about all Ceph RBD volumes in the storage cluster. If `pool` is specified, return a JSON document containing information about all Ceph RBD volumes in Ceph RBD pool `pool`. If `limit` is specified, return a JSON document containing information about all Ceph RBD volumes with names matching `limit` as fuzzy regex. ###### `POST` - * Mandatory values: `pool`, `size` + * Mandatory values: `volume`, `pool`, `size` * Optional values: N/A -Add a new Ceph RBD volume with name `` to Ceph RBD pool with name ``. `size` must be a valid size, in bytes or a single-character metric prefix of bytes, e.g. `1073741824` (1GB), `4096M`, or `20G`. +Add a new Ceph RBD volume `` to Ceph RBD pool ``. `size` must be a valid size, in bytes or a single-character metric prefix of bytes, e.g. `1073741824` (1GB), `4096M`, or `20G`. #### `/api/v1/storage/ceph/volume//` * Methods: `GET`, `DELETE` @@ -510,13 +510,13 @@ Add a new Ceph RBD volume with name `` to Ceph RBD pool with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&limit=` without fuzzy regex matching. +Return a JSON document containing information about Ceph RBD volume `` in Ceph RBD pool ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&limit=` without fuzzy regex matching. ###### `DELETE` * Mandatory values: N/A * Optional values: N/A -Remove a Ceph RBD volume with name `` from Ceph RBD pool ``. +Remove a Ceph RBD volume `` from Ceph RBD pool ``. #### `/api/v1/storage/ceph/volume/snapshot` * Methods: `GET`, `POST` @@ -525,15 +525,15 @@ Remove a Ceph RBD volume with name `` from Ceph RBD pool ``. * Mandatory values: N/A * Optional values: `pool`, `volume`, `limit` -Return a JSON document containing information about all Ceph RBD volume snapshots in the storage cluster. If `pool` is specified, return a JSON document containing information about all Ceph RBD volume snapshots in Ceph RBD pool with name `pool`. If `volume` is specified, return a JSON document containing information about all Ceph RBD volume snapshots of Ceph RBD volume with name `volume`. If `limit` is specified, return a JSON document containing information about all Ceph RBD volume snapshots with names matching `limit` as fuzzy regex. +Return a JSON document containing information about all Ceph RBD volume snapshots in the storage cluster. If `pool` is specified, return a JSON document containing information about all Ceph RBD volume snapshots in Ceph RBD pool `pool`. If `volume` is specified, return a JSON document containing information about all Ceph RBD volume snapshots of Ceph RBD volume `volume`. If `limit` is specified, return a JSON document containing information about all Ceph RBD volume snapshots with names matching `limit` as fuzzy regex. The various limit options can be combined freely, e.g. one can specify a `volume` without `pool`, which would match all snapshots of the named volume(s) regardless of pool, or a `pool` and `limit` without a `volume`, which would match all named snapshots on any volume in `pool`. ###### `POST` - * Mandatory values: `snapshot`, `pool`, `volume` + * Mandatory values: `snapshot`, `volume`, `pool` * Optional values: N/A -Add a new Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` on Ceph RBD pool with name ``. +Add a new Ceph RBD volume snapshot `snapshot` of Ceph RBD volume `volume` on Ceph RBD pool `pool`. #### `/api/v1/storage/ceph/volume/snapshot///` * Methods: `GET`, `DELETE` @@ -542,10 +542,10 @@ Add a new Ceph RBD volume snapshot with name `` of Ceph RBD volume wit * Mandatory values: N/A * Optional values: N/A -Return a JSON document containing information about Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` in Ceph RBD pool with name ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&volume=&limit=` without fuzzy regex matching. +Return a JSON document containing information about Ceph RBD volume snapshot `` of Ceph RBD volume `` in Ceph RBD pool ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&volume=&limit=` without fuzzy regex matching. ###### `DELETE` * Mandatory values: N/A * Optional values: N/A -Remove a Ceph RBD volume snapshot with name `` of Ceph RBD volume with name `` on Ceph RBD pool with name ``. +Remove a Ceph RBD volume snapshot `` of Ceph RBD volume `` on Ceph RBD pool ``. From 17b463afd433c2fda4ed07a32b72e135a70ed160 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 26 Jul 2019 11:23:19 -0400 Subject: [PATCH 14/14] Replace retcodes with 400; implement OSD status --- client-api/api_lib/pvcapi.py | 103 ++++++++++++++++++++--------------- client-api/pvc-api.py | 19 ++++--- docs/manuals/api.md | 16 +++++- 3 files changed, 84 insertions(+), 54 deletions(-) diff --git a/client-api/api_lib/pvcapi.py b/client-api/api_lib/pvcapi.py index 7ddaf084..08d8e017 100755 --- a/client-api/api_lib/pvcapi.py +++ b/client-api/api_lib/pvcapi.py @@ -41,7 +41,7 @@ def node_list(limit=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -55,7 +55,7 @@ def node_secondary(node): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -72,7 +72,7 @@ def node_primary(node): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -89,7 +89,7 @@ def node_flush(node): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -106,7 +106,7 @@ def node_ready(node): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -139,7 +139,7 @@ def vm_list(node=None, state=None, limit=None, is_fuzzy=True): 'message': 'VM not found.' } else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -160,7 +160,7 @@ def vm_define(name, xml, node, selector): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -177,7 +177,7 @@ def vm_modify(name, restart, xml): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -194,7 +194,7 @@ def vm_undefine(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -211,7 +211,7 @@ def vm_remove(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -228,7 +228,7 @@ def vm_start(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -245,7 +245,7 @@ def vm_restart(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -262,7 +262,7 @@ def vm_shutdown(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -279,7 +279,7 @@ def vm_stop(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -296,7 +296,7 @@ def vm_move(name, node, selector): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -313,7 +313,7 @@ def vm_migrate(name, node, selector, flag_force): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -330,7 +330,7 @@ def vm_unmigrate(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -350,7 +350,7 @@ def net_list(limit=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -368,7 +368,7 @@ def net_add(vni, description, nettype, domain, if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -390,7 +390,7 @@ def net_modify(vni, description, domain, if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -407,7 +407,7 @@ def net_remove(network): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -424,7 +424,7 @@ def net_dhcp_list(network, limit=None, static=False): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -438,7 +438,7 @@ def net_dhcp_add(network, ipaddress, macaddress, hostname): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -455,7 +455,7 @@ def net_dhcp_remove(network, macaddress): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -472,7 +472,7 @@ def net_acl_list(network, limit=None, direction=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -489,7 +489,7 @@ def net_acl_add(network, direction, description, rule, order): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -506,7 +506,7 @@ def net_acl_remove(network, direction, description): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -526,7 +526,7 @@ def ceph_status(): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -540,7 +540,7 @@ def ceph_radosdf(): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -554,11 +554,24 @@ def ceph_osd_list(limit=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode +def ceph_osd_state(osd): + zk_conn = pvc_common.startZKConnection(config['coordinators']) + retflag, retdata = pvc_ceph.get_list_osd(zk_conn, osd) + if retflag: + retcode = 200 + else: + retcode = 400 + + in_state = retdata['stats']['in'] + up_state = retdata['stats']['up'] + + return flask.jsonify([{ "id": osd, "in": in_state, "up": up_state }]), retcode + def ceph_osd_add(node, device, weight): """ Add a Ceph OSD to the PVC Ceph storage cluster. @@ -568,7 +581,7 @@ def ceph_osd_add(node, device, weight): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -585,7 +598,7 @@ def ceph_osd_remove(osd_id): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -602,7 +615,7 @@ def ceph_osd_in(osd_id): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -619,7 +632,7 @@ def ceph_osd_out(osd_id): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -636,7 +649,7 @@ def ceph_osd_set(option): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -653,7 +666,7 @@ def ceph_osd_unset(option): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -670,7 +683,7 @@ def ceph_pool_list(limit=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -684,7 +697,7 @@ def ceph_pool_add(name, pgs): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -701,7 +714,7 @@ def ceph_pool_remove(name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -718,7 +731,7 @@ def ceph_volume_list(pool=None, limit=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -732,7 +745,7 @@ def ceph_volume_add(pool, name, size): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -749,7 +762,7 @@ def ceph_volume_remove(pool, name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -766,7 +779,7 @@ def ceph_volume_snapshot_list(pool=None, volume=None, limit=None): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) return flask.jsonify(retdata), retcode @@ -781,7 +794,7 @@ def ceph_volume_snapshot_add(pool, volume, name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { @@ -798,7 +811,7 @@ def ceph_volume_snapshot_remove(pool, volume, name): if retflag: retcode = 200 else: - retcode = 510 + retcode = 400 pvc_common.stopZKConnection(zk_conn) output = { diff --git a/client-api/pvc-api.py b/client-api/pvc-api.py index 3e23913f..5825a170 100755 --- a/client-api/pvc-api.py +++ b/client-api/pvc-api.py @@ -622,12 +622,9 @@ def api_ceph_status(): def api_ceph_radosdf(): return pvcapi.ceph_radosdf() -@api.route('/api/v1/storage/ceph/cluster-option', methods=['GET', 'POST']) +@api.route('/api/v1/storage/ceph/cluster-option', methods=['POST']) @authenticator def api_ceph_cluster_option(): - if flask.request.method == 'GET': - pass - if flask.request.method == 'POST': # Get action if 'action' in flask.request.values: @@ -698,7 +695,7 @@ def api_ceph_osd_element(osd): @authenticator def api_ceph_osd_state(osd): if flask.request.method == 'GET': - pass + return pvcapi.ceph_osd_state(osd) if flask.request.method == 'POST': if 'state' in flask.request.values: @@ -794,13 +791,17 @@ def api_ceph_volume_root(): return pvcapi.ceph_volume_add(pool, volume, size) -@api.route('/api/v1/storage/ceph/volume//', methods=['GET', 'DELETE']) +@api.route('/api/v1/storage/ceph/volume//', methods=['GET', 'PUT', 'DELETE']) @authenticator def api_ceph_volume_element(pool, volume): if flask.request.method == 'GET': # Same as specifying /volume?limit=VOLUME return pvcapi.ceph_volume_list(pool, volume) + if flask.request.method == 'PUT': + # TODO: #44 + flask.abort(501) + if flask.request.method == 'DELETE': return pvcapi.ceph_volume_remove(pool, volume) @@ -850,13 +851,17 @@ def api_ceph_volume_snapshot_root(): return pvcapi.ceph_volume_snapshot_add(pool, volume, snapshot) -@api.route('/api/v1/storage/ceph/volume/snapshot///', methods=['GET', 'DELETE']) +@api.route('/api/v1/storage/ceph/volume/snapshot///', methods=['GET', 'PUT', 'DELETE']) @authenticator def api_ceph_volume_snapshot_element(pool, volume, snapshot): if flask.request.method == 'GET': # Same as specifying /snapshot?limit=VOLUME return pvcapi.ceph_volume_snapshot_list(pool, volume, snapshot) + if flask.request.method == 'PUT': + # TODO: #44 + flask.abort(501) + if flask.request.method == 'DELETE': return pvcapi.ceph_volume_snapshot_remove(pool, volume, snapshot) diff --git a/docs/manuals/api.md b/docs/manuals/api.md index bfef60a2..e07507fb 100644 --- a/docs/manuals/api.md +++ b/docs/manuals/api.md @@ -386,6 +386,8 @@ Return a JSON document containing information about the current Ceph cluster sta #### `/api/v1/storage/ceph/status` * Methods: `GET` + +###### `GET` * Mandatory values: N/A * Optional values: N/A @@ -393,13 +395,17 @@ This endpoint is an alias for `/api/v1/storage/ceph`. #### `/api/v1/storage/ceph/df` * Methods: `GET` + +###### `GET` * Mandatory values: N/A * Optional values: N/A Return a JSON document containing information about the current Ceph cluster utilization. The JSON element `ceph_data` contains the raw output of a `rados df` command. #### `/api/v1/storage/ceph/cluster-option` - * Methods: `GET`, `POST` + * Methods: `POST` + +###### `POST` * Mandatory values: `action`, `option` * Optional values: N/A @@ -504,7 +510,7 @@ Return a JSON document containing information about all Ceph RBD volumes in the Add a new Ceph RBD volume `` to Ceph RBD pool ``. `size` must be a valid size, in bytes or a single-character metric prefix of bytes, e.g. `1073741824` (1GB), `4096M`, or `20G`. #### `/api/v1/storage/ceph/volume//` - * Methods: `GET`, `DELETE` + * Methods: `GET`, `PUT`, `DELETE` ###### `GET` * Mandatory values: N/A @@ -512,6 +518,12 @@ Add a new Ceph RBD volume `` to Ceph RBD pool ``. `size` must be a Return a JSON document containing information about Ceph RBD volume `` in Ceph RBD pool ``. The output is identical to `/api/v1/storage/ceph/volume?pool=&limit=` without fuzzy regex matching. +###### `PUT` + * Mandatory values: N/A + * Optional values: `name`, `size` + +Change the configuration of the volume ``. If `name` is specified, rename the volume to the specified name. If `size` is specified, resize the volume to the specified size (see `POST /api/v1/storage/ceph/volume` for restrictions). + ###### `DELETE` * Mandatory values: N/A * Optional values: N/A