AzureRMR/R/az_resource.R

374 строки
14 KiB
R

#' Azure resource class
#'
#' Class representing a generic Azure resource.
#'
#' @docType class
#' @section Methods:
#' - `new(...)`: Initialize a new resource object. See 'Initialization' for more details.
#' - `delete(..., confirm=TRUE, wait=FALSE)`: Delete this resource, after a confirmation check. Optionally wait for the delete to finish.
#' - `update(...)`: Update this resource on the host.
#' - `sync_fields()`: Update the fields in this object with information from the host. Returns the `properties$provisioningState` field, so you can query this programmatically to check if a resource has finished provisioning. Not all resource types require explicit provisioning, in which case this method will return NULL.
#' - `set_api_version(api_version)`: Set the API version to use when interacting with the host. By default, use the latest API version available.
#' - `do_operation(...)` Carry out an operation. See 'Operations' for more details.
#'
#' @section Initialization:
#' There are multiple ways to initialize a new resource object. The `new()` method can retrieve an existing resource, deploy/create a new resource, or create an empty/null object (without communicating with the host), based on the arguments you supply.
#'
#' All of these initialization options have the following arguments in common.
#' 1. `token`: An OAuth 2.0 token, as generated by [get_azure_token].
#' 2. `subscription`: The subscription ID.
#' 3. `api_version`: Optionally, the API version to use when interacting with the host. By default, this is NULL in which case the latest API version will be used.
#' 4. A set of _identifying arguments_:
#' - `resource_group`: The resource group containing the resource.
#' - `id`: The full ID of the resource. This is a string of the form `/subscriptions/{uuid}/resourceGroups/{resource-group-name}/provider/{resource-provider-name}/{resource-path}/{resource-name}`.
#' - `provider`: The provider of the resource, eg `Microsoft.Compute`.
#' - `path`: The path to the resource, eg `virtualMachines`.
#' - `type`: The combination of provider and path, eg `Microsoft.Compute/virtualMachines`.
#' - `name`: The name of the resource instance, eg `myWindowsVM`.
#'
#' Providing `id` will fill in the values for all the other identifying arguments. Similarly, providing `type` will fill in the values for `provider` and `path`. Unless you provide `id`, you must also provide `name`.
#'
#' The default behaviour for `new()` is to retrieve an existing resource, which occurs if you supply only the arguments listed above. If you also supply an argument `deployed_properties=NULL`, this will create a null object. If you supply any other (named) arguments, `new()` will create a new object on the host, with the supplied arguments as parameters.
#'
#' Generally, the easiest way to initialize an object is via the `get_resource`, `create_resource` or `list_resources` methods of the [az_resource_group] class, which will handle all the gory details automatically.
#'
#' @section Operations:
#' The `do_operation()` method allows you to carry out arbitrary operations on the resource. It takes the following arguments:
#' - `op`: The operation in question, which will be appended to the URL path of the request.
#' - `options`: A named list giving the URL query parameters.
#' - `...`: Other named arguments passed to [call_azure_rm], and then to the appropriate call in httr. In particular, use `body` to supply the body of a PUT, POST or PATCH request.
#' - `http_verb`: The HTTP verb as a string, one of `GET`, `PUT`, `POST`, `DELETE`, `HEAD` or `PATCH`.
#'
#' Consult the Azure documentation for your resource to find out what operations are supported.
#'
#' @seealso
#' [az_resource_group], [call_azure_rm], [call_azure_url],
#' [Resources API reference](https://docs.microsoft.com/en-us/rest/api/resources/resources)
#'
#' @examples
#' \dontrun{
#'
#' # recommended way to retrieve a resource: via a resource group object
#' # storage account:
#' stor <- resgroup$get_resource(type="Microsoft.Storage/storageAccounts", name="mystorage")
#' # virtual machine:
#' vm <- resgroup$get_resource(type="Microsoft.Compute/virtualMachines", name="myvm")
#'
#' ## carry out operations on a resource
#'
#' # storage account: get access keys
#' stor$do_operation("listKeys", http_verb="POST")
#'
#' # virtual machine: run a script
#' vm$do_operation("runCommand",
#' body=list(
#' commandId="RunShellScript", # RunPowerShellScript for Windows
#' script=as.list("ifconfig > /tmp/ifconfig.out")
#' ),
#' encode="json",
#' http_verb="POST")
#'
#' ## retrieve properties
#'
#' # storage account: endpoint URIs
#' stor$properties$primaryEndpoints$file
#' stor$properties$primaryEndpoints$blob
#'
#' # virtual machine: hardware profile
#' vm$properties$hardwareProfile
#'
#' ## update a resource: resizing a VM
#' properties <- list(hardwareProfile=list(vmSize="Standard_DS3_v2"))
#' vm$do_operation(http_verb="PATCH",
#' body=list(properties=properties),
#' encode="json")
#'
#' # sync with Azure: useful to track resource creation/update status
#' vm$sync_fields()
#'
#' # delete a resource
#' stor$delete()
#'
#' }
#' @format An R6 object of class `az_resource`.
#' @export
az_resource <- R6::R6Class("az_resource",
public=list(
subscription=NULL,
resource_group=NULL,
type=NULL,
name=NULL,
id=NULL,
identity=NULL,
kind=NULL,
location=NULL,
managed_by=NULL,
plan=NULL,
properties=NULL,
sku=NULL,
tags=NULL,
token=NULL,
etag=NULL,
# constructor overloads:
# 1. deploy resource: resgroup, {provider, path}|type, name, ...
# 2. deploy resource by id: id, ...
# 3. get from passed-in data: deployed_properties
# 4. get from host: resgroup, {provider, path}|type, name
# 5. get from host by id: id
initialize=function(token, subscription, resource_group, provider, path, type, name, id, ...,
deployed_properties=list(), api_version=NULL, wait=FALSE)
{
self$token <- token
self$subscription <- subscription
private$init_id_fields(resource_group, provider, path, type, name, id, deployed_properties)
# by default this is unset at initialisation, for efficiency
private$api_version <- api_version
parms <- if(!is_empty(list(...)))
private$init_and_deploy(..., wait=wait)
else if(!is_empty(deployed_properties))
private$init_from_parms(deployed_properties)
else private$init_from_host()
self$identity <- parms$identity
self$kind <- parms$kind
self$location <- parms$location
self$managed_by <- parms$managedBy
self$plan <- parms$plan
self$properties <- parms$properties
self$sku <- parms$sku
self$tags <- parms$tags
self$etag <- parms$etag
NULL
},
# API versions vary across different providers; find the latest for this resource
set_api_version=function(api_version=NULL)
{
if(!is_empty(api_version))
{
private$api_version <- api_version
return()
}
slash <- regexpr("/", self$type)
provider <- substr(self$type, 1, slash - 1)
path <- substr(self$type, slash + 1, nchar(self$type))
op <- construct_path("providers", provider)
apis <- named_list(call_azure_rm(self$token, self$subscription, op)$resourceTypes, "resourceType")
names(apis) <- tolower(names(apis))
private$api_version <- apis[[tolower(path)]]$apiVersions[[1]]
if(is_empty(private$api_version))
stop("Unable to retrieve API version for resource '", self$type, "'.", call.=FALSE)
invisible(private$api_version)
},
sync_fields=function()
{
self$initialize(self$token, self$subscription, id=self$id)
self$properties$provisioningState
},
delete=function(..., options=list(), confirm=TRUE, wait=FALSE)
{
if(confirm && interactive())
{
yn <- readline(paste0("Do you really want to delete resource '", self$type, "/", self$name, "'? (y/N) "))
if(tolower(substr(yn, 1, 1)) != "y")
return(invisible(NULL))
}
message("Deleting resource '", construct_path(self$type, self$name), "'")
private$res_op(..., options=options, http_verb="DELETE")
if(wait)
{
for(i in 1:1000)
{
status <- httr::status_code(private$res_op(http_status_handler="pass"))
if(status >= 300)
break
Sys.sleep(5)
}
if(status < 300)
warning("Attempt to delete resource did not succeed", call.=FALSE)
}
invisible(NULL)
},
do_operation=function(..., options=list(), http_verb="GET")
{
private$res_op(..., options=options, http_verb=http_verb)
},
update=function(..., options=list())
{
parms <- list(...)
private$validate_update_parms(names(parms))
private$res_op(body=parms, options=options, encode="json", http_verb="PATCH")
self$sync_fields()
},
set_tags=function(..., keep_existing=TRUE)
{
tags <- match.call(expand.dots=FALSE)$...
unvalued <- names(tags) == ""
values <- lapply(seq_along(unvalued), function(i)
{
if(unvalued[i]) "" else as.character(eval.parent(tags[[i]]))
})
names(values) <- ifelse(unvalued, as.character(tags), names(tags))
if(keep_existing)
values <- modifyList(self$tags, values)
if(is.null(values))
values <- list()
self$update(tags=values)
},
get_tags=function()
{
self$tags
},
print=function(...)
{
# generate label from id, since type and name are not guaranteed to be fixed for sub-resources
cat("<Azure resource ", sub("^.+providers/(.+$)", "\\1", self$id), ">\n", sep="")
cat(format_public_fields(self, exclude=c("subscription", "resource_group", "type", "name")))
cat(format_public_methods(self))
invisible(NULL)
}
),
private=list(
api_version=NULL,
# initialise identifier fields from multiple ways of constructing object
init_id_fields=function(resource_group, provider, path, type, name, id, parms=list())
{
# if these are supplied, use to fill in everything else
if(!is_empty(parms$id) && !is_empty(parms$type) && !is_empty(parms$name))
{
resource_group <- sub("^.+resourceGroups/([^/]+)/.*$", "\\1", parms$id, ignore.case=TRUE)
type <- parms$type
name <- parms$name
id <- parms$id
}
else if(!missing(id))
{
resource_group <- sub("^.+resourceGroups/([^/]+)/.*$", "\\1", id, ignore.case=TRUE)
id2 <- sub("^.+providers/", "", id)
type_delim <- attr(regexpr("^[^/]+/[^/]+/", id2), "match.length")
type <- substr(id2, 1, type_delim - 1)
name <- substr(id2, type_delim + 1, nchar(id2))
}
else
{
if(missing(type))
type <- construct_path(provider, path)
id <- construct_path("/subscriptions", self$subscription, "resourceGroups", resource_group,
"providers", type, name)
}
self$resource_group <- resource_group
self$type <- type
self$name <- name
self$id <- id
},
init_from_parms=function(parms)
{
# allow list(NULL) as special case for creating an empty object
if(!identical(parms, list(NULL)))
private$validate_response_parms(parms)
parms
},
init_from_host=function()
{
private$res_op()
},
init_and_deploy=function(..., wait)
{
properties <- list(...)
# check if we were passed a json object
if(length(properties) == 1 && is.character(properties[[1]]) && jsonlite::validate(properties[[1]]))
properties <- jsonlite::fromJSON(properties[[1]], simplifyVector=FALSE)
private$validate_deploy_parms(properties)
private$res_op(body=properties, encode="json", http_verb="PUT")
# do we wait until resource has finished provisioning?
if(wait)
{
message("Waiting for provisioning to complete")
for(i in 1:1000) # some resources can take a long time to provision (AKS, Kusto)
{
Sys.sleep(5)
res <- private$res_op()
status <- res$properties$provisioningState
if(status %in% c("Succeeded", "Error", "Failed"))
break
message(".", appendLF=FALSE)
}
if(status == "Succeeded")
message("\nDeployment successful")
else stop("\nUnable to create resource", call.=FALSE)
}
else
{
# allow time for provisioning setup, then get properties
Sys.sleep(2)
res <- private$res_op()
}
res
},
validate_deploy_parms=function(parms)
{
required_names <- character(0)
optional_names <-
c("identity", "kind", "location", "managedBy", "plan", "properties", "sku", "tags", "etag")
validate_object_names(names(parms), required_names, optional_names)
},
validate_response_parms=function(parms)
{
required_names <- c("id", "name", "type", "location")
optional_names <- c("identity", "kind", "managedBy", "plan", "properties", "sku", "tags", "etag")
validate_object_names(names(parms), required_names, optional_names)
},
validate_update_parms=function(parms)
{
required_names <- character(0)
optional_names <-
c("identity", "kind", "location", "managedBy", "plan", "properties", "sku", "tags", "etag")
validate_object_names(names(parms), required_names, optional_names)
},
res_op=function(op="", ..., api_version=private$api_version)
{
# make sure we have an API to call
if(is.null(private$api_version))
self$set_api_version()
op <- construct_path("resourcegroups", self$resource_group, "providers", self$type, self$name, op)
call_azure_rm(self$token, self$subscription, op, ..., api_version=api_version)
}
))