import base64
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from linode_api4.common import load_and_validate_keys
from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import (
Firewall,
Instance,
InstanceDiskEncryptionType,
Kernel,
PlacementGroup,
StackScript,
Type,
)
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.objects.filtering import Filter
from linode_api4.objects.linode import (
Backup,
InstancePlacementGroupAssignment,
InterfaceGeneration,
NetworkInterface,
_expand_placement_group_assignment,
)
from linode_api4.objects.linode_interfaces import LinodeInterfaceOptions
from linode_api4.util import drop_null_keys
[docs]
class LinodeGroup(Group):
"""
Encapsulates Linode-related methods of the :any:`LinodeClient`. This
should not be instantiated on its own, but should instead be used through
an instance of :any:`LinodeClient`::
client = LinodeClient(token)
instances = client.linode.instances() # use the LinodeGroup
This group contains all features beneath the `/linode` group in the API v4.
"""
[docs]
def types(self, *filters):
"""
Returns a list of Linode Instance types. These may be used to create
or resize Linodes, or simply referenced on their own. Types can be
filtered to return specific types, for example::
standard_types = client.linode.types(Type.class == "standard")
API documentation: https://techdocs.akamai.com/linode-api/reference/get-linode-types
:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.
:returns: A list of types that match the query.
:rtype: PaginatedList of Type
"""
return self.client._get_and_filter(Type, *filters)
[docs]
def instances(self, *filters):
"""
Returns a list of Linode Instances on your account. You may filter
this query to return only Linodes that match specific criteria::
prod_linodes = client.linode.instances(Instance.group == "prod")
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-linode-instances
:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.
:returns: A list of Instances that matched the query.
:rtype: PaginatedList of Instance
"""
return self.client._get_and_filter(Instance, *filters)
[docs]
def stackscripts(self, *filters, **kwargs):
"""
Returns a list of :any:`StackScripts<StackScript>`, both public and
private. You may filter this query to return only
:any:`StackScripts<StackScript>` that match certain criteria. You may
also request only your own private :any:`StackScripts<StackScript>`::
my_stackscripts = client.linode.stackscripts(mine_only=True)
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-stack-scripts
:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.
:param mine_only: If True, returns only private StackScripts
:type mine_only: bool
:returns: A list of StackScripts matching the query.
:rtype: PaginatedList of StackScript
"""
# python2 can't handle *args and a single keyword argument, so this is a workaround
if "mine_only" in kwargs:
if kwargs["mine_only"]:
new_filter = Filter({"mine": True})
if filters:
filters = list(filters)
filters[0] = filters[0] & new_filter
else:
filters = [new_filter]
del kwargs["mine_only"]
if kwargs:
raise TypeError(
"stackscripts() got unexpected keyword argument '{}'".format(
kwargs.popitem()[0]
)
)
return self.client._get_and_filter(StackScript, *filters)
[docs]
def kernels(self, *filters):
"""
Returns a list of available :any:`Kernels<Kernel>`. Kernels are used
when creating or updating :any:`LinodeConfigs,LinodeConfig>`.
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-kernels
:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.
:returns: A list of available kernels that match the query.
:rtype: PaginatedList of Kernel
"""
return self.client._get_and_filter(Kernel, *filters)
# create things
[docs]
def instance_create(
self,
ltype,
region,
image=None,
authorized_keys=None,
firewall: Optional[Union[Firewall, int]] = None,
backup: Optional[Union[Backup, int]] = None,
stackscript: Optional[Union[StackScript, int]] = None,
disk_encryption: Optional[
Union[InstanceDiskEncryptionType, str]
] = None,
placement_group: Optional[
Union[
InstancePlacementGroupAssignment,
PlacementGroup,
Dict[str, Any],
int,
]
] = None,
interfaces: Optional[
List[
Union[LinodeInterfaceOptions, NetworkInterface, Dict[str, Any]],
]
] = None,
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
network_helper: Optional[bool] = None,
maintenance_policy: Optional[str] = None,
root_pass: Optional[str] = None,
kernel: Optional[str] = None,
boot_size: Optional[int] = None,
authorized_users: Optional[List[str]] = None,
ipv4: Optional[List[str]] = None,
**kwargs,
):
"""
Creates a new Linode Instance. This function has several modes of operation:
**Create an Instance from an Image**
To create an Instance from an :any:`Image`, call `instance_create` with
a :any:`Type`, a :any:`Region`, and an :any:`Image`. All three of
these fields may be provided as either the ID or the appropriate object.
When an Image is provided, at least one of ``root_pass``, ``authorized_users``, or
``authorized_keys`` must also be given.
For example::
new_linode = client.linode.instance_create(
"g6-standard-2",
"us-east",
image="linode/debian13",
root_pass="aComplex@Password123")
ltype = client.linode.types().first()
region = client.regions().first()
image = client.images().first()
another_linode = client.linode.instance_create(
ltype,
region,
image=image,
authorized_keys="ssh-rsa AAAA")
To output the first IPv4 address of the new Linode:
print(new_linode.ipv4[0])
To delete the new_linode (WARNING: this immediately destroys the Linode):
new_linode.delete()
**Create an Instance from StackScript**
When creating an Instance from a :any:`StackScript`, an :any:`Image` that
the StackScript support must be provided.. You must also provide any
required StackScript data for the script's User Defined Fields.. For
example, if deploying `StackScript 10079`_ (which deploys a new Instance
with a user created from keys on `github`_::
stackscript = StackScript(client, 10079)
new_linode = client.linode.instance_create(
"g6-standard-2",
"us-east",
image="linode/debian13",
root_pass="aComplex@Password123",
stackscript=stackscript,
stackscript_data={"gh_username": "example"})
In the above example, "gh_username" is the name of a User Defined Field
in the chosen StackScript. For more information on StackScripts, see
the `StackScript guide`_.
.. _`StackScript 10079`: https://www.linode.com/stackscripts/view/10079
.. _`github`: https://github.com
.. _`StackScript guide`: https://www.linode.com/docs/platform/stackscripts/
**Create an Instance from a Backup**
To create a new Instance by restoring a :any:`Backup` to it, provide a
:any:`Type`, a :any:`Region`, and the :any:`Backup` to restore. You
may provide either IDs or objects for all of these fields::
existing_linode = Instance(client, 123)
snapshot = existing_linode.available_backups.snapshot.current
new_linode = client.linode.instance_create(
"g6-standard-2",
"us-east",
backup=snapshot)
**Create an Instance with explicit interfaces:**
To create a new Instance with explicit interfaces, provide list of
LinodeInterfaceOptions objects or dicts to the "interfaces" field::
linode = client.linode.instance_create(
"g6-standard-1",
"us-mia",
image="linode/ubuntu24.04",
root_pass="aComplex@Password123",
# This can be configured as an account-wide default
interface_generation=InterfaceGeneration.LINODE,
interfaces=[
LinodeInterfaceOptions(
default_route=LinodeInterfaceDefaultRouteOptions(
ipv4=True,
ipv6=True
),
public=LinodeInterfacePublicOptions
)
]
)
**Create an empty Instance**
If you want to create an empty Instance that you will configure manually,
simply call `instance_create` with a :any:`Type` and a :any:`Region`::
empty_linode = client.linode.instance_create("g6-standard-2", "us-east")
When created this way, the Instance will not be booted and cannot boot
successfully until disks and configs are created, or it is otherwise
configured.
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-linode-instance
:param ltype: The Instance Type we are creating
:type ltype: str or Type
:param region: The Region in which we are creating the Instance
:type region: str or Region
:param image: The Image to deploy to this Instance. If this is provided,
at least one of root_pass, authorized_users, or authorized_keys must also be
provided.
:type image: str or Image
:param root_pass: The root password for the new Instance. Required when
an image is provided and neither authorized_users nor
authorized_keys are given.
:type root_pass: str
:param stackscript: The StackScript to deploy to the new Instance. If
provided, "image" is required and must be compatible
with the chosen StackScript.
:type stackscript: int or StackScript
:param stackscript_data: Values for the User Defined Fields defined in
the chosen StackScript. Does nothing if
StackScript is not provided.
:type stackscript_data: dict
:param backup: The Backup to restore to the new Instance. May not be
provided if "image" is given.
:type backup: int of Backup
:param authorized_keys: The ssh public keys to install in the linode's
/root/.ssh/authorized_keys file. Each entry may
be a single key, or a path to a file containing
the key.
:type authorized_keys: list or str
:param authorized_users: A list of usernames whose keys should be installed
as trusted for the root user. These user's keys
should already be set up, see :any:`ProfileGroup.ssh_keys`
for details.
:type authorized_users: list[str]
:param label: The display label for the new Instance
:type label: str
:param group: The display group for the new Instance
:type group: str
:param booted: Whether the new Instance should be booted. This will
default to True if the Instance is deployed from an Image
or Backup.
:type booted: bool
:param tags: A list of tags to apply to the new instance. If any of the
tags included do not exist, they will be created as part of
this operation.
:type tags: list[str]
:param private_ip: Whether the new Instance should have private networking
enabled and assigned a private IPv4 address.
:type private_ip: bool
:param metadata: Metadata-related fields to use when creating the new Instance.
The contents of this field can be built using the
:any:`build_instance_metadata` method.
:type metadata: dict
:param firewall: The firewall to attach this Linode to.
:type firewall: int or Firewall
:param disk_encryption: The disk encryption policy for this Linode.
:type disk_encryption: InstanceDiskEncryptionType or str
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
At least one and up to three Interface objects can exist in this array.
:type interfaces: List[LinodeInterfaceOptions], List[NetworkInterface], or List[dict[str, Any]]
:param placement_group: A Placement Group to create this Linode under.
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
:param interface_generation: The generation of network interfaces this Linode uses.
:type interface_generation: InterfaceGeneration or str
:param network_helper: Whether this instance should have Network Helper enabled.
:type network_helper: bool
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
If not provided, the default policy (linode/migrate) will be applied.
:type maintenance_policy: str
:param kernel: The kernel to boot the Instance with. If provided, this will be used as the
kernel for the default configuration profile.
:type kernel: str
:param boot_size: The size of the boot disk in MB. If provided, this will be used to create
the boot disk for the Instance.
:type boot_size: int
:param ipv4: A list of reserved IPv4 addresses to assign to this Instance.
NOTE: Reserved IP feature may not currently be available to all users.
:type ipv4: list[str]
:returns: A new Instance object
:rtype: Instance
:raises ApiError: If contacting the API fails
:raises UnexpectedResponseError: If the API response is somehow malformed.
This usually indicates that you are using
an outdated library.
"""
if (
image
and not root_pass
and not authorized_keys
and not authorized_users
):
raise ValueError(
"When creating an Instance from an Image, at least one of "
"root_pass, authorized_users, or authorized_keys must be provided."
)
params = {
"type": ltype,
"region": region,
"image": image,
"root_pass": root_pass,
"authorized_keys": load_and_validate_keys(authorized_keys),
"authorized_users": authorized_users,
# These will automatically be flattened below
"firewall_id": firewall,
"backup_id": backup,
"stackscript_id": stackscript,
"maintenance_policy": maintenance_policy,
# Special cases
"disk_encryption": (
str(disk_encryption) if disk_encryption else None
),
"placement_group": (
_expand_placement_group_assignment(placement_group)
if placement_group
else None
),
"interfaces": interfaces,
"interface_generation": interface_generation,
"network_helper": network_helper,
"kernel": kernel,
"boot_size": boot_size,
"ipv4": ipv4,
}
params.update(kwargs)
result = self.client.post(
"/linode/instances",
data=_flatten_request_body_recursive(drop_null_keys(params)),
)
if not "id" in result:
raise UnexpectedResponseError(
"Unexpected response when creating linode!", json=result
)
return Instance(self.client, result["id"], result)
[docs]
def stackscript_create(
self, label, script, images, desc=None, public=False, **kwargs
):
"""
Creates a new :any:`StackScript` on your account.
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-add-stack-script
:param label: The label for this StackScript.
:type label: str
:param script: The script to run when an :any:`Instance` is deployed with
this StackScript. Must begin with a shebang (#!).
:type script: str
:param images: A list of :any:`Images<Image>` that this StackScript
supports. Instances will not be deployed from this
StackScript unless deployed from one of these Images.
:type images: list of Image
:param desc: A description for this StackScript.
:type desc: str
:param public: Whether this StackScript is public. Defaults to False.
Once a StackScript is made public, it may not be set
back to private.
:type public: bool
:returns: The new StackScript
:rtype: StackScript
"""
script_body = script
if not script.startswith("#!"):
# it doesn't look like a stackscript body, let's see if it's a file
script_path = Path(script)
if script_path.is_file():
with open(script_path) as f:
script_body = f.read()
else:
raise ValueError(
"script must be the script text or a path to a file"
)
params = {
"label": label,
"images": images,
"is_public": public,
"script": script_body,
"description": desc if desc else "",
}
params.update(kwargs)
result = self.client.post(
"/linode/stackscripts",
data=_flatten_request_body_recursive(params),
)
if not "id" in result:
raise UnexpectedResponseError(
"Unexpected response when creating StackScript!", json=result
)
s = StackScript(self.client, result["id"], result)
return s