class User(Base): api_endpoint = "/account/users/{id}" id_attribute = 'username' properties = { 'email': Property(mutable=True), 'username': Property(identifier=True, mutable=True), 'restricted': Property(mutable=True), } @property def grants(self): from linode.objects.account import UserGrants if not hasattr(self, '_grants'): resp = self._client.get(UserGrants.api_endpoint.format(username=self.username)) grants = UserGrants(self._client, self.username, resp) self._set('_grants', grants) return self._grants def invalidate(self): if hasattr(self, '_grants'): del self._grants Base.invalidate(self) def change_password(self, password): """ Sets this user's password """ result = self._client.post('{}/password'.format(User.api_endpoint), model=self, data={ "password": password }) return True
class User(Base): api_endpoint = "/account/users/{id}" id_attribute = 'username' properties = { 'email': Property(), 'username': Property(identifier=True, mutable=True), 'restricted': Property(mutable=True), } @property def grants(self): """ Retrieves the grants for this user. If the user is unrestricted, this will result in an ApiError. This is smart, and will only fetch from the api once unless the object is invalidated. :returns: The grants for this user. :rtype: linode.objects.account.UserGrants """ from linode.objects.account import UserGrants if not hasattr(self, '_grants'): resp = self._client.get( UserGrants.api_endpoint.format(username=self.username)) grants = UserGrants(self._client, self.username, resp) self._set('_grants', grants) return self._grants def invalidate(self): if hasattr(self, '_grants'): del self._grants Base.invalidate(self)
class IPv6Pool(Base): api_endpoint = '/networking/ipv6/pools/{}' id_attribute = 'range' properties = { 'range': Property(identifier=True), 'region': Property(slug_relationship=Region, filterable=True), }
class LongviewSubscription(Base): api_endpoint = 'longview/subscriptions/{id}' properties = { "id": Property(identifier=True), "label": Property(), "clients_included": Property(), "price": Property() }
class Payment(Base): api_endpoint = "/account/payments/{id}" properties = { "id": Property(identifier=True), "date": Property(is_datetime=True), "amount": Property(), }
class WhitelistEntry(Base): api_endpoint = "/profile/whitelist/{id}" properties = { 'id': Property(identifier=True), 'address': Property(), 'netmask': Property(), 'note': Property(), }
class Invoice(Base): api_endpoint = "/account/invoices/{id}" properties = { "id": Property(identifier=True), "label": Property(), "date": Property(is_datetime=True), "total": Property(), }
class AccountSettings(Base): api_endpoint = "/account/settings" id_attribute = 'managed' # this isn't actually used properties = { "network_helper": Property(mutable=True), "managed": Property(), "longview_subscription": Property(slug_relationship=LongviewSubscription) }
class IPAddress(Base): api_endpoint = '/networking/ips/{address}' id_attribute = 'address' properties = { "address": Property(identifier=True), "gateway": Property(), "subnet_mask": Property(), "prefix": Property(), "type": Property(), "public": Property(), "rdns": Property(mutable=True), "linode_id": Property(), "region": Property(slug_relationship=Region, filterable=True), } @property def linode(self): from ..linode import Linode if not hasattr(self, '_linode'): self._set('_linode', Linode(self._client, self.linode_id)) return self._linode def to(self, linode): """ This is a helper method for ip-assign, and should not be used outside of that context. It's used to cleanly build an IP Assign request with pretty python syntax. """ from ..linode import Linode if not isinstance(linode, Linode): raise ValueError("IP Address can only be assigned to a Linode!") return {"address": self.address, "linode_id": linode.id}
class Kernel(Base): api_endpoint = "/linode/kernels/{id}" properties = { "created": Property(is_datetime=True), "deprecated": Property(filterable=True), "description": Property(), "id": Property(identifier=True), "kvm": Property(filterable=True), "label": Property(filterable=True), "updates": Property(), "version": Property(filterable=True), "architecture": Property(filterable=True), "xen": Property(filterable=True), }
class NodeBalancerNode(DerivedBase): api_endpoint = '/nodebalancers/{nodebalancer_id}/configs/{config_id}/nodes/{id}' derived_url_path = 'nodes' parent_id_name = 'config_id' properties = { 'id': Property(identifier=True), 'config_id': Property(identifier=True), 'nodebalancer_id': Property(identifier=True), "label": Property(mutable=True), "address": Property(mutable=True), "weight": Property(mutable=True), "mode": Property(mutable=True), "status": Property(), } def __init__(self, client, id, parent_id, nodebalancer_id=None, json=None): """ We need a special constructor here because this object's parent has a parent itself. """ if not nodebalancer_id and not isinstance(parent_id, tuple): raise ValueError( 'NodeBalancerNode must either be created with a nodebalancer_id or a tuple of ' '(config_id, nodebalancer_id) for parent_id!') if isinstance(parent_id, tuple): nodebalancer_id = parent_id[1] parent_id = parent_id[0] DerivedBase.__init__(self, client, id, parent_id, json=json) self._set('nodebalancer_id', nodebalancer_id)
class Type(Base): api_endpoint = "/linode/types/{id}" properties = { 'disk': Property(filterable=True), 'id': Property(identifier=True), 'label': Property(filterable=True), 'network_out': Property(filterable=True), 'price': Property(), 'addons': Property(), 'memory': Property(filterable=True), 'transfer': Property(filterable=True), 'vcpus': Property(filterable=True), }
class IPv6Address(Base): api_endpoint = 'networking/ipv6/{address}' id_attribute = 'address' properties = { "address": Property(identifier=True), "gateway": Property(), "range": Property(), "rdns": Property(mutable=True), "prefix": Property(), "subnet_mask": Property(), "type": Property(), "region": Property(slug_relationship=Region), }
class OAuthToken(Base): api_name = 'tokens' api_endpoint = "/account/tokens/{id}" properties = { "id": Property(identifier=True), "client": Property(relationship=OAuthClient), "type": Property(), "scopes": Property(), "label": Property(mutable=True), "created": Property(is_datetime=True), "token": Property(), "expiry": Property(is_datetime=True), }
class AuthorizedApp(Base): api_endpoint = "/profile/apps/{id}" properties = { "id": Property(identifier=True), "scopes": Property(), "label": Property(), "created": Property(is_datetime=True), "expiry": Property(is_datetime=True), "thumbnail_url": Property(), "website": Property(), }
class LongviewClient(Base): api_endpoint = '/longview/clients/{id}' properties = { "id": Property(identifier=True), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), "label": Property(mutable=True, filterable=True), "install_code": Property(), "apps": Property(), "api_key": Property(), }
class InvoiceItem(DerivedBase): api_endpoint = '/account/invoices/{invoice_id}/items' derived_url_path = 'items' parent_id_name = 'invoice_id' # TODO - this object doesn't have its own ID .. this might need # special handling properties = { 'invoice_id': Property(identifier=True), 'unit_price': Property(), 'label': Property(), 'amount': Property(), 'quantity': Property(), 'from': Property(is_datetime=True), 'to': Property(is_datetime=True), 'type': Property(), }
class Disk(DerivedBase): api_endpoint = '/linode/instances/{linode_id}/disks/{id}' derived_url_path = 'disks' parent_id_name = 'linode_id' properties = { 'id': Property(identifier=True), 'created': Property(is_datetime=True), 'label': Property(mutable=True, filterable=True), 'size': Property(filterable=True), 'status': Property(filterable=True), 'filesystem': Property(), 'updated': Property(is_datetime=True), 'linode_id': Property(identifier=True), } def duplicate(self): result = self._client.post(Disk.api_endpoint, model=self, data={}) if not 'id' in result: raise UnexpectedResponseError( 'Unexpected response duplicating disk!', json=result) d = Disk(self._client, result['id'], self.linode_id, result) return d def reset_root_password(self, root_password=None): rpass = root_password if not rpass: from linode.objects.linode import Linode rpass = Linode.generate_root_password() params = { 'password': rpass, } result = self._client.post(Disk.api_endpoint, model=self, data=params) if not 'id' in result: raise UnexpectedResponseError( 'Unexpected response duplicating disk!', json=result) self._populate(result) if not root_password: return True, rpass return True
class PersonalAccessToken(Base): api_endpoint = "/profile/tokens/{id}" properties = { "id": Property(identifier=True), "scopes": Property(), "label": Property(mutable=True), "created": Property(is_datetime=True), "token": Property(), "expiry": Property(is_datetime=True), }
class TicketReply(DerivedBase): api_endpoint = '/support/tickets/{ticket_id}/replies' derived_url_path = 'replies' parent_id_name = 'ticket_id' properties = { 'id': Property(identifier=True), 'ticket_id': Property(identifier=True), 'description': Property(), 'created': Property(is_datetime=True), 'created_by': Property(), 'from_linode': Property(), }
class StackScript(Base): api_endpoint = '/linode/stackscripts/{id}' properties = { "user_defined_fields": Property(), "label": Property(mutable=True, filterable=True), "rev_note": Property(mutable=True), "usernam": Property(filterable=True), "user_gravatar_id": Property(), "is_public": Property(mutable=True, filterable=True), "created": Property(is_datetime=True), "deployments_active": Property(), "script": Property(mutable=True), "images": Property(mutable=True, filterable=True), # TODO make slug_relationship "deployments_total": Property(), "description": Property(mutable=True, filterable=True), "updated": Property(is_datetime=True), } def _populate(self, json): """ Override the populate method to map user_defined_fields to fancy values """ Base._populate(self, json) mapped_udfs = [] for udf in self.user_defined_fields: t = UserDefinedFieldType.text choices = None if hasattr(udf, 'oneof'): t = UserDefinedFieldType.select_one choices = udf.oneof.split(',') elif hasattr(udf, 'manyof'): t = UserDefinedFieldType.select_many choices = udf.manyof.split(',') mapped_udfs.append( UserDefinedField( udf.name, udf.label if hasattr(udf, 'label') else None, udf.example if hasattr(udf, 'example') else None, t, choices=choices)) self._set('user_defined_fields', mapped_udfs) ndist = [Image(self._client, d) for d in self.images] self._set('images', ndist) def _serialize(self): dct = Base._serialize(self) dct['images'] = [d.id for d in self.images] return dct
class AccountSettings(Base): api_name = 'settings' # should never come up api_endpoint = "/account/settings" id_attribute = 'email' properties = { "company": Property(mutable=True), "country": Property(mutable=True), "balance": Property(), "address_1": Property(mutable=True), "network_helper": Property(mutable=True), "last_name": Property(mutable=True), "city": Property(mutable=True), "state": Property(mutable=True), "first_name": Property(mutable=True), "phone": Property(mutable=True), "managed": Property(), "email": Property(mutable=True), "zip": Property(mutable=True), "address_2": Property(mutable=True), }
class DomainRecord(DerivedBase): api_name = "records" api_endpoint = "/domains/{domain_id}/records/{id}" derived_url_path = "records" parent_id_name = "domain_id" properties = { 'id': Property(identifier=True), 'domain_id': Property(identifier=True), 'type': Property(), 'name': Property(mutable=True, filterable=True), 'target': Property(mutable=True, filterable=True), 'priority': Property(mutable=True), 'weight': Property(mutable=True), 'port': Property(mutable=True), 'service': Property(mutable=True), 'protocol': Property(mutable=True), 'ttl_sec': Property(mutable=True), }
class Event(Base): api_endpoint = '/account/events/{id}' properties = { 'id': Property(identifier=True), 'percent_complete': Property(volatile=True), 'created': Property(is_datetime=True, filterable=True), 'updated': Property(is_datetime=True, filterable=True), 'seen': Property(), 'read': Property(), 'action': Property(), 'user_id': Property(), 'username': Property(), 'entity': Property(), 'time_remaining': Property(), 'rate': Property(), 'status': Property(), } @property def linode(self): if self.entity and self.entity.type == 'linode': return Linode(self._client, self.entity.id) return None @property def stackscript(self): if self.entity and self.entity.type == 'stackscript': return StackScript(self._client, self.entity.id) return None @property def domain(self): if self.entity and self.entity.type == 'domain': return Domain(self._client, self.entity.id) return None @property def nodebalancer(self): if self.entity and self.entity.type == 'nodebalancer': return NodeBalancer(self._client, self.entity.id) return None @property def ticket(self): if self.entity and self.entity.type == 'ticket': return SupportTicket(self._client, self.entity.id) return None @property def volume(self): if self.entity and self.entity.type == 'volume': return Volume(self._client, self.entity.id) return None def mark_read(self): self._client.post('{}/read'.format(Event.api_endpoint), model=self)
class Account(Base): api_endpoint = "/account" id_attribute = 'email' properties = { "company": Property(mutable=True), "country": Property(mutable=True), "balance": Property(), "address_1": Property(mutable=True), "last_name": Property(mutable=True), "city": Property(mutable=True), "state": Property(mutable=True), "first_name": Property(mutable=True), "phone": Property(mutable=True), "email": Property(mutable=True), "zip": Property(mutable=True), "address_2": Property(mutable=True), "tax_id": Property(mutable=True), }
class Domain(Base): api_endpoint = "/domains/{id}" properties = { 'id': Property(identifier=True), 'domain': Property(mutable=True, filterable=True), 'group': Property(mutable=True, filterable=True), 'description': Property(mutable=True), 'status': Property(mutable=True), 'soa_email': Property(mutable=True), 'retry_sec': Property(mutable=True), 'master_ips': Property(mutable=True, filterable=True), 'axfr_ips': Property(mutable=True), 'expire_sec': Property(mutable=True), 'refresh_sec': Property(mutable=True), 'ttl_sec': Property(mutable=True), 'records': Property(derived_class=DomainRecord), 'type': Property(mutable=True), } def create_record(self, record_type, **kwargs): params = { "type": record_type, } params.update(kwargs) result = self._client.post("{}/records".format(Domain.api_endpoint), model=self, data=params) self.invalidate() if not 'id' in result: raise UnexpectedResponseError( 'Unexpected response creating domain record!', json=result) zr = DomainRecord(self._client, result['id'], self.id, result) return zr
class Linode(Base): api_endpoint = '/linode/instances/{id}' properties = { 'id': Property(identifier=True, filterable=True), 'label': Property(mutable=True, filterable=True), 'group': Property(mutable=True, filterable=True), 'status': Property(volatile=True), 'created': Property(is_datetime=True), 'updated': Property(volatile=True, is_datetime=True), 'region': Property(slug_relationship=Region, filterable=True), 'alerts': Property(), 'image': Property(slug_relationship=Image, filterable=True), 'disks': Property(derived_class=Disk), 'configs': Property(derived_class=Config), 'type': Property(slug_relationship=Type), 'backups': Property(), 'ipv4': Property(), 'ipv6': Property(), 'hypervisor': Property(), 'specs': Property(), } @property def ips(self): """ The ips related collection is not normalized like the others, so we have to make an ad-hoc object to return for its response """ if not hasattr(self, '_ips'): result = self._client.get("{}/ips".format(Linode.api_endpoint), model=self) if not "ipv4" in result: raise UnexpectedResponseError('Unexpected response loading IPs', json=result) v4pub = [] for c in result['ipv4']['public']: i = IPAddress(self._client, c['address'], c) v4pub.append(i) v4pri = [] for c in result['ipv4']['private']: i = IPAddress(self._client, c['address'], c) v4pri.append(i) shared_ips = [] for c in result['ipv4']['shared']: i = IPAddress(self._client, c['address'], c) shared_ips.append(i) slaac = IPAddress(self._client, result['ipv6']['slaac']['address'], result['ipv6']['slaac']) link_local = IPAddress(self._client, result['ipv6']['link_local']['address'], result['ipv6']['link_local']) pools = [] for p in result['ipv6']['global']: pools.append(IPv6Pool(self._client, p['range'])) ips = MappedObject(**{ "ipv4": { "public": v4pub, "private": v4pri, "shared": shared_ips, }, "ipv6": { "slaac": slaac, "link_local": link_local, "pools": pools, }, }) self._set('_ips', ips) return self._ips @property def available_backups(self): """ The backups response contains what backups are available to be restored. """ if not hasattr(self, '_avail_backups'): result = self._client.get("{}/backups".format(Linode.api_endpoint), model=self) if not 'automatic' in result: raise UnexpectedResponseError('Unexpected response loading available backups!', json=result) automatic = [] for a in result['automatic']: cur = Backup(self._client, a['id'], self.id, a) automatic.append(cur) snap = None if result['snapshot']['current']: snap = Backup(self._client, result['snapshot']['current']['id'], self.id, result['snapshot']['current']) psnap = None if result['snapshot']['in_progress']: psnap = Backup(self._client, result['snapshot']['in_progress']['id'], self.id, result['snapshot']['in_progress']) self._set('_avail_backups', MappedObject(**{ "automatic": automatic, "snapshot": { "current": snap, "in_progress": psnap, } })) return self._avail_backups def _populate(self, json): if json is not None: # fixes ipv4 and ipv6 attribute of json to make base._populate work if 'ipv4' in json and 'address' in json['ipv4']: json['ipv4']['id'] = json['ipv4']['address'] if 'ipv6' in json and isinstance(json['ipv6'], list): for j in json['ipv6']: j['id'] = j['range'] Base._populate(self, json) def invalidate(self): """ Clear out cached properties """ if hasattr(self, '_avail_backups'): del self._avail_backups if hasattr(self, '_ips'): del self._ips Base.invalidate(self) def boot(self, config=None): resp = self._client.post("{}/boot".format(Linode.api_endpoint), model=self, data={'config_id': config.id} if config else None) if 'error' in resp: return False return True def shutdown(self): resp = self._client.post("{}/shutdown".format(Linode.api_endpoint), model=self) if 'error' in resp: return False return True def reboot(self): resp = self._client.post("{}/reboot".format(Linode.api_endpoint), model=self) if 'error' in resp: return False return True @staticmethod def generate_root_password(): def _func(value): if sys.version_info[0] < 3: value = int(value.encode('hex'), 16) return value password = ''.join([ PASSWORD_CHARS[_func(c) % len(PASSWORD_CHARS)] for c in urandom(randint(50, 128)) ]) return password # create derived objects def create_config(self, kernel=None, label=None, devices=[], disks=[], volumes=[], **kwargs): """ Creates a Linode Config with the given attributes. :param kernel: The kernel to boot with. :param label: The config label :param disks: The list of disks, starting at sda, to map to this config. :param volumes: The volumes, starting after the last disk, to map to this config :param devices: A list of devices to assign to this config, in device index order. Values must be of type Disk or Volume. If this is given, you may not include disks or volumes. :param **kwargs: Any other arguments accepted by the api. :returns: A new Linode Config """ from ..volume import Volume hypervisor_prefix = 'sd' if self.hypervisor == 'kvm' else 'xvd' device_names = [hypervisor_prefix + string.ascii_lowercase[i] for i in range(0, 8)] device_map = {device_names[i]: None for i in range(0, len(device_names))} if devices and (disks or volumes): raise ValueError('You may not call create_config with "devices" and ' 'either of "disks" or "volumes" specified!') if not devices: if not isinstance(disks, list): disks = [disks] if not isinstance(volumes, list): volumes = [volumes] devices = [] for d in disks: if d is None: devices.append(None) elif isinstance(d, Disk): devices.append(d) else: devices.append(Disk(self._client, int(d), self.id)) for v in volumes: if v is None: devices.append(None) elif isinstance(v, Volume): devices.append(v) else: devices.append(Volume(self._client, int(v))) if not devices: raise ValueError('Must include at least one disk or volume!') for i, d in enumerate(devices): if d is None: pass elif isinstance(d, Disk): device_map[device_names[i]] = {'disk_id': d.id } elif isinstance(d, Volume): device_map[device_names[i]] = {'volume_id': d.id } else: raise TypeError('Disk or Volume expected!') params = { 'kernel': kernel.id if issubclass(type(kernel), Base) else kernel, 'label': label if label else "{}_config_{}".format(self.label, len(self.configs)), 'devices': device_map, } params.update(kwargs) result = self._client.post("{}/configs".format(Linode.api_endpoint), model=self, data=params) self.invalidate() if not 'id' in result: raise UnexpectedResponseError('Unexpected response creating config!', json=result) c = Config(self._client, result['id'], self.id, result) return c def create_disk(self, size, label=None, filesystem=None, read_only=False, image=None, root_pass=None, authorized_keys=None, stackscript=None, **stackscript_args): gen_pass = None if image and not root_pass: gen_pass = Linode.generate_root_password() root_pass = gen_pass authorized_keys = load_and_validate_keys(authorized_keys) if image and not label: label = "My {} Disk".format(image.label) params = { 'size': size, 'label': label if label else "{}_disk_{}".format(self.label, len(self.disks)), 'read_only': read_only, 'filesystem': filesystem if filesystem else 'raw', 'authorized_keys': authorized_keys, } if image: params.update({ 'image': image.id if issubclass(type(image), Base) else image, 'root_pass': root_pass, }) if stackscript: params['stackscript_id'] = stackscript.id if stackscript_args: params['stackscript_data'] = stackscript_args result = self._client.post("{}/disks".format(Linode.api_endpoint), model=self, data=params) self.invalidate() if not 'id' in result: raise UnexpectedResponseError('Unexpected response creating disk!', json=result) d = Disk(self._client, result['id'], self.id, result) if gen_pass: return d, gen_pass return d def enable_backups(self): """ Enable Backups for this Linode. When enabled, we will automatically backup your Linode's data so that it can be restored at a later date. For more information on Linode's Backups service and pricing, see our `Backups Page`_ .. _Backups Page: https://www.linode.com/backups """ self._client.post("{}/backups/enable".format(Linode.api_endpoint), model=self) self.invalidate() return True def cancel_backups(self): """ Cancels Backups for this Linode. All existing Backups will be lost, including any snapshots that have been taken. This cannot be undone, but Backups can be re-enabled at a later date. """ self._client.post("{}/backups/cancel".format(Linode.api_endpoint), model=self) self.invalidate() return True def snapshot(self, label=None): result = self._client.post("{}/backups".format(Linode.api_endpoint), model=self, data={ "label": label }) if not 'id' in result: raise UnexpectedResponseError('Unexpected response taking snapshot!', json=result) # so the changes show up the next time they're accessed if hasattr(self, '_avail_backups'): del self._avail_backups b = Backup(self._client, result['id'], self.id, result) return b def allocate_ip(self, public=False): """ Allocates a new :any:`IPAddress` for this Linode. Additional public IPs require justification, and you may need to open a :any:`SupportTicket` before you can add one. You may only have, at most, one private IP per Linode. :param public: If the new IP should be public or private. Defaults to private. :type public: bool :returns: The new IPAddress :rtype: IPAddress """ result = self._client.post( "{}/ips".format(Linode.api_endpoint), model=self, data={ "type": "ipv4", "public": public, }) if not 'address' in result: raise UnexpectedResponseError('Unexpected response allocating IP!', json=result) i = IPAddress(self._client, result['address'], result) return i def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs): """ Rebuilding a Linode deletes all existing Disks and Configs and deploys a new :any:`Image` to it. This can be used to reset an existing Linode or to install an Image on an empty Linode. :param image: The Image to deploy to this Linode :type image: str or Image :param root_pass: The root password for the newly rebuilt Linode. If omitted, a password will be generated and returned. :type root_pass: str :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 :returns: The newly generated password, if one was not provided (otherwise True) :rtype: str or bool """ ret_pass = None if not root_pass: ret_pass = Linode.generate_root_password() root_pass = ret_pass authorized_keys = load_and_validate_keys(authorized_keys) params = { 'image': image.id if issubclass(type(image), Base) else image, 'root_pass': root_pass, 'authorized_keys': authorized_keys, } params.update(kwargs) result = self._client.post('{}/rebuild'.format(Linode.api_endpoint), model=self, data=params) if not 'id' in result: raise UnexpectedResponseError('Unexpected response issuing rebuild!', json=result) # update ourself with the newly-returned information self._populate(result) if not ret_pass: return True else: return ret_pass def rescue(self, *disks): if disks: disks = { x: { 'disk_id': y } for x,y in zip(('sda','sdb','sdc','sdd','sde','sdf','sdg'), disks) } else: disks=None result = self._client.post('{}/rescue'.format(Linode.api_endpoint), model=self, data={ "devices": disks }) return result def kvmify(self): """ Converts this linode to KVM from Xen """ self._client.post('{}/kvmify'.format(Linode.api_endpoint), model=self) return True def mutate(self): """ Upgrades this Linode to the latest generation type """ self._client.post('{}/mutate'.format(Linode.api_endpoint), model=self) return True def clone(self, to_linode=None, region=None, service=None, configs=[], disks=[], label=None, group=None, with_backups=None): """ Clones this linode into a new linode or into a new linode in the given region """ if to_linode and region: raise ValueError('You may only specify one of "to_linode" and "region"') if region and not service: raise ValueError('Specifying a region requires a "service" as well') if not isinstance(configs, list) and not isinstance(configs, PaginatedList): configs = [configs] if not isinstance(disks, list) and not isinstance(disks, PaginatedList): disks = [disks] cids = [ c.id if issubclass(type(c), Base) else c for c in configs ] dids = [ d.id if issubclass(type(d), Base) else d for d in disks ] params = { "linode_id": to_linode.id if issubclass(type(to_linode), Base) else to_linode, "region": region.id if issubclass(type(region), Base) else region, "type": service.id if issubclass(type(service), Base) else service, "configs": cids if cids else None, "disks": dids if dids else None, "label": label, "group": group, "with_backups": with_backups, } result = self._client.post('{}/clone'.format(Linode.api_endpoint), model=self, data=params) if not 'id' in result: raise UnexpectedResponseError('Unexpected response cloning Linode!', json=result) l = Linode(self._client, result['id'], result) return l @property def stats(self): """ Returns the JSON stats for this Linode """ # TODO - this would be nicer if we formatted the stats return self._client.get('{}/stats'.format(Linode.api_endpoint), model=self) def stats_for(self, dt): """ Returns stats for the month containing the given datetime """ # TODO - this would be nicer if we formatted the stats if not isinstance(dt, datetime): raise TypeError('stats_for requires a datetime object!') return self._client.get('{}/stats/'.format(dt.strftime('%Y/%m')))
class Image(Base): """ An Image is something a Linode or Disk can be deployed from. """ api_endpoint = '/images/{id}' properties = { "id": Property(identifier=True), "label": Property(mutable=True), "description": Property(mutable=True), "status": Property(), "created": Property(is_datetime=True), "created_by": Property(), "type": Property(), "is_public": Property(), "vendor": Property(), "size": Property(), "deprecated": Property() }
class Profile(Base): api_endpoint = "/profile" id_attribute = 'username' properties = { 'username': Property(identifier=True), 'uid': Property(), 'email': Property(mutable=True), 'timezone': Property(mutable=True), 'email_notifications': Property(mutable=True), 'referrals': Property(), 'ip_whitelist_enabled': Property(mutable=True), 'lish_auth_method': Property(mutable=True), 'authorized_keys': Property(mutable=True), 'two_factor_auth': Property(), 'restricted': Property(), } def enable_tfa(self): """ Enables TFA for the token's user. This requies a follow-up request to confirm TFA. Returns the TFA secret that needs to be confirmed. """ result = self._client.post('/profile/tfa-enable') return result['secret'] def confirm_tfa(self, code): """ Confirms TFA for an account. Needs a TFA code generated by enable_tfa """ self._client.post('/profile/tfa-enable-confirm', data={"tfa_code": code}) return True def disable_tfa(self): """ Turns off TFA for this user's account. """ self._client.post('/profile/tfa-disable') return True @property def grants(self): """ Returns grants for the current user """ from linode.objects.account import UserGrants resp = self._client.get( '/profile/grants') # use special endpoint for restricted users grants = None if resp is not None: # if resp is None, we're unrestricted and do not have grants grants = UserGrants(self._client, self.username, resp) return grants @property def whitelist(self): """ Returns the user's whitelist entries, if whitelist is enabled """ return self._client._get_and_filter(WhitelistEntry) def add_whitelist_entry(self, address, netmask, note=None): """ Adds a new entry to this user's IP whitelist, if enabled """ result = self._client.post("{}/whitelist".format(Profile.api_endpoint), data={ "address": address, "netmask": netmask, "note": note, }) if not 'id' in result: raise UnexpectedResponseError( "Unexpected response creating whitelist entry!") return WhitelistEntry(result['id'], self._client, json=result)
class NodeBalancerConfig(DerivedBase): api_endpoint = '/nodebalancers/{nodebalancer_id}/configs/{id}' derived_url_path = 'configs' parent_id_name = 'nodebalancer_id' properties = { 'id': Property(identifier=True), 'nodebalancer_id': Property(identifier=True), "port": Property(mutable=True), "protocol": Property(mutable=True), "algorithm": Property(mutable=True), "stickiness": Property(mutable=True), "check": Property(mutable=True), "check_interval": Property(mutable=True), "check_timeout": Property(mutable=True), "check_attempts": Property(mutable=True), "check_path": Property(mutable=True), "check_body": Property(mutable=True), "check_passive": Property(mutable=True), "ssl_cert": Property(mutable=True), "ssl_key": Property(mutable=True), "ssl_commonname": Property(), "ssl_fingerprint": Property(), "cipher_suite": Property(mutable=True), "nodes_status": Property(), } @property def nodes(self): """ This is a special derived_class relationship because NodeBalancerNode is the only api object that requires two parent_ids """ if not hasattr(self, '_nodes'): base_url = "{}/{}".format(NodeBalancerConfig.api_endpoint, NodeBalancerNode.derived_url_path) result = self._client._get_objects( base_url, NodeBalancerNode, model=self, parent_id=(self.id, self.nodebalancer_id)) self._set('_nodes', result) return self._nodes def create_node(self, label, address, **kwargs): params = { "label": label, "address": address, } params.update(kwargs) result = self._client.post("{}/nodes".format( NodeBalancerConfig.api_endpoint), model=self, data=params) self.invalidate() if not 'id' in result: raise UnexpectedResponseError('Unexpected response creating node!', json=result) # this is three levels deep, so we need a special constructor n = NodeBalancerNode(self._client, result['id'], self.id, self.nodebalancer_id, result) return n def load_ssl_data(self, cert_file, key_file): """ A convenience method that loads a cert and a key from files and sets them on this object. This can make enabling ssl easier (instead of you needing to load the files yourself). This does *not* change protocol/port for you, or save anything. Once this is called, you must still call `save()` on this object for the changes to take effect. :param cert_file: A path to the file containing the public certificate :type cert_file: str :param key_file: A path to the file containing the unpassphrased private key :type key_file: str """ # we're disabling warnings here because these attributes are defined dynamically # through linode.objects.Base, and pylint isn't privy if os.path.isfile(os.path.expanduser(cert_file)): with open(os.path.expanduser(cert_file)) as f: self.ssl_cert = f.read() # pylint: disable=attribute-defined-outside-init if os.path.isfile(os.path.expanduser(key_file)): with open(os.path.expanduser(key_file)) as f: self.ssl_key = f.read() # pylint: disable=attribute-defined-outside-init