├── .coveragerc ├── .envrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── MANIFEST.in ├── OWNERS.md ├── README.md ├── RELEASE.md ├── SUPPORT.md ├── packet ├── BGPConfig.py ├── BGPSession.py ├── Batch.py ├── Device.py ├── DeviceBatch.py ├── Email.py ├── Event.py ├── Facility.py ├── HardwareReservation.py ├── IPAddress.py ├── Manager.py ├── Metro.py ├── OperatingSystem.py ├── Organization.py ├── Plan.py ├── Project.py ├── Provider.py ├── SSHKey.py ├── Snapshot.py ├── Vlan.py ├── Volume.py ├── __init__.py └── baseapi.py ├── setup.cfg ├── setup.py ├── shell.nix ├── test ├── fixtures │ ├── get__capacity.json │ ├── get_devices_9dec7266.json │ ├── get_devices_e123s_ips.json │ ├── get_facilities.json │ ├── get_ips_e123s.json │ ├── get_locations_metros.json │ ├── get_operating-systems.json │ ├── get_plans.json │ ├── get_projects.json │ ├── get_projects_1234_bgp-config.json │ ├── get_projects_438659f0.json │ ├── get_projects_438659f0_devices.json │ ├── get_projects_438659f0_ips.json │ ├── get_projects_438659f0_storage.json │ ├── get_projects_438659f1_ips.json │ ├── get_ssh-keys.json │ ├── get_ssh-keys_084a5dec.json │ ├── get_storage_f9a8a263.json │ ├── get_storage_f9a8a263_snapshots.json │ ├── get_user.json │ ├── patch_devices_e781ae1b.json │ ├── patch_projects_438659f0.json │ ├── patch_ssh-keys_084a5dec.json │ ├── patch_storage_f9a8a263.json │ ├── post__capacity.json │ ├── post__capacity_metros.json │ ├── post_devices_e781ae1b_actions.json │ ├── post_projects.json │ ├── post_projects_438659f0_devices.json │ ├── post_projects_438659f0_storage.json │ ├── post_ssh-keys.json │ ├── post_storage_f9a8a263_attachments.json │ ├── post_storage_f9a8a263_clone.json │ └── post_storage_f9a8a263_snapshots.json ├── test_baseapi.py ├── test_batch.py ├── test_device.py ├── test_email.py ├── test_event.py ├── test_ips.py ├── test_organization.py ├── test_packet.py ├── test_ports.py ├── test_vlan.py ├── test_volume.py └── test_vpn.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = packet 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | def __repr__ 9 | if .debug: 10 | raise NotImplementedError 11 | if __name__ == .__main__.: 12 | ignore_errors = True 13 | omit = 14 | .tox/* 15 | test/* 16 | setup.py 17 | 18 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | which nix &>/dev/null && use nix 2 | unset SOURCE_DATE_EPOCH 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: 13 | - '3.10' 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - id: fmt_and_lint 22 | run: | 23 | pip install black==22.6.0 pylama 24 | black --check --diff . 25 | pylama packet test setup.py 26 | 27 | test: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | python-version: 32 | - 2.7 33 | - 3.8 34 | - 3.9 35 | - '3.10' 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Set up Python ${{ matrix.python-version }} 40 | uses: actions/setup-python@v2 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | - name: Install Dependencies 44 | run: | 45 | python -m pip install --upgrade setuptools pip wheel 46 | pip install tox tox-gh-actions 47 | - name: Test with tox 48 | run: | 49 | tox 50 | find . # TODO: remove this 51 | # - name: Upload coverage.xml 52 | # if: ${{ matrix.python-version == '3.10' }} 53 | # uses: actions/upload-artifact@v2 54 | # with: 55 | # name: tox-gh-actions-coverage 56 | # path: coverage.xml 57 | # if-no-files-found: error 58 | - uses: codecov/codecov-action@v2 59 | if: ${{ matrix.python-version == '3.10' }} 60 | with: 61 | flags: unittests # optional 62 | name: codecov-umbrella # optional 63 | fail_ci_if_error: true # optional (default = false) 64 | verbose: true # optional (default = false) 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .idea/ 9 | .vscode/ 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | enable/ 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | unittest_*.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). 5 | This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.45.0] - Unreleased 8 | ### Added 9 | ### Changed 10 | ### Fixed 11 | 12 | ## [1.44.3] - 2022-07-27 13 | ### Fixed 14 | - Fix handling non-dict error data in ResponseError #128 #129 15 | 16 | ## [1.44.2] - 2021-11-18 17 | ### Fixed 18 | - Fix logic behind `validate_metro_capacity` #125 19 | 20 | ## [1.44.1] - 2021-09-20 21 | ### Fixed 22 | - Fix metros URL used in Metro API lists #122 23 | - Catch TypeError when HardwareReservations do not have a device assignment #120 24 | 25 | ## [1.44.0] - 2021-05-19 26 | ### Added 27 | - User-Agent header added to client requests #113 28 | - `Metro` class added (https://feedback.equinixmetal.com/changelog/new-metros-feature-live) #110 29 | - Adds `metro` property to `DeviceBatch`, `Device`, `IPAddress`, `VLan` #110 30 | - Adds `metro` to `create_device`, `reserve_ip_address`, `create_vlan`, `create_connection` #110 31 | - Adds `list_metros`, `validate_metro_capacity` to `Manager` #110 32 | - Adds `CODE_OF_CONDUCT.md`, `SUPPORT.md`, `OWNERS.md` #102, #101, #100 33 | - Adds package metadata for `author`, `author_email`, `copyright` #114 34 | ### Changed 35 | - `facility` is now optional in `create_device`, `reserve_ip_address`, `create_vlan`, `create_connection` #110 36 | - CI is using GH Actions instead of Drone #115 37 | ### Fixed 38 | - Handles when IPAddress Facility is null in API responses #117 39 | 40 | ## [1.43.1] - 2020-09-04 41 | ### Fixed 42 | - ResponseError fixed for Python2.7 compatibility 43 | 44 | ## [1.43.0] - 2020-07-14 45 | ### Added 46 | - Support for reinstalling the operating system to a device, including changing the operating system. 47 | - `Manager.create_vlan` now includes a description argument 48 | ### Changed 49 | - `ResponseError` will now be raised when an API call results in an error 50 | ### Fixed 51 | - `Manager.validate_capacity` now considers availability 52 | - `Manager.create_project_ssh_key` will retry when it encounters 404 responses following a successful creation. 53 | - API responses with `{"error":""}` keys were not handled well, and will now be handled just like `{"errors":[""]}` keys. 54 | 55 | ## [1.42.0] - 2020-02-14 56 | ### Added 57 | - Capturing of `available_in` to Plan 58 | - Capturing of `hardware_reservation`, `spot_price_max`, `termination_time`, and `provisioning_percentage` to `Device` 59 | - Support for creating project ssh keys 60 | - Support for passing `custom_data` when creating a device 61 | ### Fixed 62 | - Black not building for CI and thus failing 63 | 64 | ## [1.41.0] - 2019-10-16 65 | ### Added 66 | - Support for retrieval of hardware reservations 67 | - CPR support at device creation 68 | 69 | ## [1.40.0] - 2019-10-14 70 | ### Added 71 | - Integration tests are only run if `PACKET_PYTHON_TEST_ACTUAL_API` envvar is set 72 | - Rescue action and along with test 73 | - Missing SPDX and source encoding meta comments 74 | ### Removed 75 | - Use of Travis CI 76 | 77 | ## [1.39.1] - 2019-09-17 78 | ### Added 79 | - Support for `hardware_reservation_id` 80 | 81 | ## [1.39.0] - 2019-08-26 82 | ### Added 83 | - Support for Organizations, Events, Emails, VLAN, Snapshot Policies, Batches, Ports, VPN and IPs. 84 | - Live tests 85 | 86 | ## [1.38.2] - 2019-05-30 87 | ### Added 88 | - Test fixtures to sdist 89 | 90 | ## [1.38.1] - 2019-05-30 91 | ### Fixed 92 | - Changelog 93 | 94 | ## [1.38.0] - 2019-05-30 95 | ### Added 96 | - Support for python3.7 97 | - `legacy` param to `get_capacity` function 98 | ### Removed 99 | - Support for python3.3 100 | ### Changed 101 | - setup.py no longer converts markdown to reST because pypi now supports markdown, woop. 102 | 103 | ## [1.37.1] - 2018-01-08 104 | ### Fixed 105 | - Version number in setup.py 106 | 107 | ## [1.37.0] - 2018-01-08 108 | ### Added 109 | - Spot Market Support 110 | - Ability to specify ssh keys on device creation 111 | 112 | ## [1.36.0] - 2017-10-16 113 | ### Added 114 | - Better tests using PacketMockManager 115 | - Test on 2.7 and 3.[3-6] 116 | - Changelog 117 | 118 | ### Changed 119 | - Use tox for testing 120 | 121 | ## [1.35] - 2017-08-04 122 | ### Fixed 123 | - Some tests were broken 124 | 125 | ## [1.35] 126 | ### Added 127 | - `public_ipv4_subnet_size` 128 | 129 | ## [1.34] - 2017-08-04 130 | ### Added 131 | - Custom iPXE and `always_pxe` setting 132 | - Volume coloning 133 | - Device Tags 134 | 135 | ### Fixed 136 | - Handling of error messages from api response 137 | 138 | ## [1.33] - 2017-03-15 139 | ### Fixed 140 | - Default payment method 141 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@equinixmetal.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | include test/fixtures/*.json 4 | -------------------------------------------------------------------------------- /OWNERS.md: -------------------------------------------------------------------------------- 1 | # Owners 2 | 3 | This project is governed by [Equinix Metal](https://metal.equinix.com) and benefits from a community of users that collaborate and contribute to its use in Kubernetes on Equinix Metal. 4 | 5 | Members of the Equinix Metal Github organization will strive to triage issues in a timely manner, see [SUPPORT.md](SUPPORT.md) for details. 6 | 7 | See the [packethost/standards glossary](https://github.com/packethost/standards/blob/master/glossary.md#ownersmd) for more details about this file. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Equinix Metal 2 | 3 | A Python client for the Equinix Metal API. 4 | 5 | [![Build Status](https://github.com/packethost/packet-python/actions/workflows/test.yml/badge.svg)](https://github.com/packethost/packet-python/actions/workflows/test.yml) 6 | [![Stability: Maintained](https://img.shields.io/badge/Stability-Maintained-green.svg)](https://github.com/packethost/standards/blob/main/maintained-statement.md#maintained-statements) 7 | 8 | This repository is [Maintained](https://github.com/packethost/standards/blob/master/maintained-statement.md) meaning that this software is supported by Equinix Metal and its community - available to use in production environments. 9 | 10 | ## Table of Contents 11 | 12 | * [Installation](#installation) 13 | * [Documentation](#documentation) 14 | * [Authentication](#authentication) 15 | * [Examples](#examples) 16 | * [List Projects](#list-projects) 17 | * [List Plans](#list-plans) 18 | * [Creating a Device](#creating-a-device) 19 | * [Checking the Status and Rebooting a Device](#checking-the-status-and-rebooting-a-device) 20 | * [Listing all Devices Limiting to 50 per Page](#listing-all-devices-limiting-to-50-per-page) 21 | * [Updating a Device](#updating-a-device) 22 | * [Deleting a Device](#deleting-a-device) 23 | * [Creating a Device Batch](#creating-a-device-batch) 24 | * [Creating a Volume](#creating-a-volume) 25 | * [Attaching and Detaching a Volume](#attaching-and-detaching-a-volume) 26 | * [Creating and Restoring a Volume Snapshot](#creating-and-restoring-a-volume-snapshot) 27 | * [Listing Project IP Addresses](#listing-project-ip-addresses) 28 | * [Creating a Project for an Organization](#creating-a-project-for-an-organization) 29 | * [Creating a VLAN](#creating-a-vlan) 30 | * [Contributing](#contributing) 31 | * [Copyright](#copyright) 32 | * [Changes](#changes) 33 | 34 | ## Installation 35 | 36 | The Equinix Metal python api library can be installed using pip: 37 | 38 | pip install packet-python 39 | 40 | Package information available here: 41 | 42 | https://pypi.python.org/pypi/packet-python 43 | 44 | ## Documentation 45 | 46 | Full Equinix Metal API documenation is available here: 47 | [https://metal.equinix.com/developers/api/](https://metal.equinix.com/developers/api/) 48 | 49 | ## Authentication 50 | 51 | Provide your credentials when instantiating client: 52 | 53 | ```python 54 | import packet 55 | manager = packet.Manager(auth_token="yourapiauthtoken") 56 | ``` 57 | 58 | ## Examples 59 | 60 | ### List Projects 61 | 62 | ```python 63 | import packet 64 | manager = packet.Manager(auth_token="yourapiauthtoken") 65 | 66 | projects = manager.list_projects() 67 | for project in projects: 68 | print(project) 69 | ``` 70 | 71 | ### List Plans 72 | 73 | ```python 74 | import packet 75 | manager = packet.Manager(auth_token="yourapiauthtoken") 76 | 77 | plans = manager.list_plans() 78 | for plan in plans: 79 | print(plan) 80 | if 'cpus' in plan.specs: 81 | print(plan.specs['cpus'][0]['count']) 82 | ``` 83 | 84 | ### Creating a Device 85 | 86 | ```python 87 | import packet 88 | manager = packet.Manager(auth_token="yourapiauthtoken") 89 | 90 | device = manager.create_device(project_id='project-id', 91 | hostname='node-name-of-your-choice', 92 | plan='baremetal_1', metro='sv', 93 | operating_system='ubuntu_18_04') 94 | print(device) 95 | ``` 96 | 97 | ### Checking the Status and Rebooting a Device 98 | 99 | ```python 100 | import packet 101 | manager = packet.Manager(auth_token="yourapiauthtoken") 102 | 103 | device = manager.get_device('device-id') 104 | print(device.state) 105 | device.reboot() 106 | ``` 107 | 108 | ### Listing all Devices Limiting to 50 per Page 109 | 110 | _Equinix Metal API defaults to a limit of 10 per page_ 111 | 112 | ```python 113 | import packet 114 | manager = packet.Manager(auth_token="yourapiauthtoken") 115 | params = { 116 | 'per_page': 50 117 | } 118 | devices = manager.list_devices(project_id='project_id', params = params) 119 | print(devices) 120 | ``` 121 | 122 | ### Updating a Device 123 | 124 | ```python 125 | import packet 126 | manager = packet.Manager(auth_token="yourapiauthtoken") 127 | 128 | device = manager.get_device('device-id') 129 | device.hostname = "test02" 130 | device.description = "new description" 131 | 132 | device.update() 133 | ``` 134 | 135 | ### Deleting a Device 136 | 137 | ```python 138 | import packet 139 | manager = packet.Manager(auth_token="yourapiauthtoken") 140 | 141 | device = manager.get_device('device-id') 142 | device.delete() 143 | ``` 144 | 145 | ### Creating a Device Batch 146 | 147 | ```python 148 | import packet 149 | manager = packet.Manager(auth_token="yourapiauthtoken") 150 | 151 | batch01 = packet.DeviceBatch({ 152 | "hostname": "batch01", 153 | "quantity": 2, 154 | "facility": "ams1", 155 | "operating_system": "centos_7", 156 | "plan": "baremetal_0", 157 | }) 158 | 159 | device_batch = manager.create_batch(project_id="project_id", params=[batch01]) 160 | print(device_batch) 161 | ``` 162 | 163 | ### Creating a Volume 164 | 165 | ```python 166 | import packet 167 | manager = packet.Manager(auth_token="yourapiauthtoken") 168 | 169 | volume = manager.create_volume(project_id="project-id", 170 | description="volume description", 171 | plan="storage_1", 172 | size="100", 173 | facility="ewr1", 174 | snapshot_count=7, 175 | snapshot_frequency="1day") 176 | print(volume) 177 | ``` 178 | 179 | ### Attaching and Detaching a Volume 180 | 181 | ```python 182 | import packet 183 | import time 184 | 185 | manager = packet.Manager(auth_token="yourapiauthtoken") 186 | volume = manager.get_volume("volume_id") 187 | 188 | volume.attach("device_id") 189 | 190 | while True: 191 | if manager.get_device("device_id").state == "active": 192 | break 193 | time.sleep(2) 194 | 195 | volume.detach() 196 | ``` 197 | 198 | ## Creating and Restoring a Volume Snapshot 199 | 200 | ```python 201 | import packet 202 | import time 203 | 204 | manager = packet.Manager(auth_token="yourapiauthtoken") 205 | 206 | volume = manager.get_volume("volume_id") 207 | volume.create_snapshot() 208 | 209 | while True: 210 | if manager.get_volume(volume.id).state == "active": 211 | break 212 | time.sleep(2) 213 | 214 | snapshots = manager.get_snapshots(volume.id) 215 | volume.restore(snapshots[0].timestamp) 216 | ``` 217 | 218 | ### Listing Project IP Addresses 219 | 220 | ```python 221 | import packet 222 | manager = packet.Manager(auth_token="yourapiauthtoken") 223 | 224 | ips = manager.list_project_ips("project_id") 225 | for ip in ips: 226 | print(ip.address) 227 | ``` 228 | 229 | ### Creating a Project for an Organization 230 | 231 | ```python 232 | import packet 233 | manager = packet.Manager(auth_token="yourapiauthtoken") 234 | 235 | project = manager.create_organization_project( 236 | org_id="organization_id", 237 | name="Integration Tests", 238 | customdata={"tag": "QA"} 239 | ) 240 | print(project) 241 | ``` 242 | 243 | ### Creating a VLAN 244 | 245 | ```python 246 | import packet 247 | manager = packet.Manager(auth_token="yourapiauthtoken") 248 | 249 | vlan = manager.create_vlan(project_id="project_id", facility="ewr1") 250 | print(vlan) 251 | ``` 252 | 253 | ## Contributing 254 | 255 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 256 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 257 | * Fork the project. 258 | * Start a feature/bugfix branch. 259 | * Commit and push until you are happy with your contribution. 260 | * You can test your changes with the `tox`, which is what GitHub Actions use to check builds. 261 | 262 | ## Credits 263 | 264 | CargoCulted with much gratitude from: 265 | https://github.com/koalalorenzo/python-digitalocean 266 | 267 | ## Changes 268 | 269 | See the [Changelog](CHANGELOG.md) for further details. 270 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Instructions 2 | 3 | These build and release instructions are intended for the maintainers and future maintainers of this project. 4 | 5 | ## Preparing a new version 6 | 7 | * a version update in `packet/__init__.py` 8 | * Update CHANGELOG.md with the new release notes 9 | * Add a stub for the next set of Unreleased changes 10 | * Create a PR with these changes 11 | * Merge 12 | 13 | ## Tagging and Building 14 | 15 | Pull down the merged changes: 16 | 17 | ```bash 18 | git fetch origin 19 | git checkout origin/master 20 | ``` 21 | 22 | Tag the commit: 23 | 24 | ```bash 25 | git tag -a vAA.BB.CC origin/master -m vAA.BB.CC # use -s if gpg is available 26 | ``` 27 | 28 | Build the package using `setuptools`: 29 | 30 | ```bash 31 | python setup.py sdist bdist_wheel 32 | ``` 33 | 34 | ## Publishing 35 | 36 | Make sure you have `~/.pypirc` correctly populated, as of today should look something like: 37 | 38 | ``` 39 | [distutils] 40 | index-servers = 41 | pypi 42 | testpypi 43 | 44 | [pypi] 45 | username: username-here 46 | password: password-here 47 | 48 | [testpypi] 49 | repository: https://test.pypi.org/legacy/ 50 | username: username-here (not necessarily same as real pypi) 51 | password: password-here (not necessarily same as real pypi) 52 | ``` 53 | 54 | If you are using a token, the username is `__token__`. Make sure you have been added to the 55 | [project](https://pypi.org/manage/project/packet-python/collaboration/) and the 56 | [test project](https://test.pypi.org/manage/project/packet-python/collaboration/). 57 | 58 | Then upload using twine to testpypi first: 59 | 60 | ```bash 61 | twine upload -r testpypi dist/* 62 | ``` 63 | 64 | If everything looks good, push the tag to GH, and then push to the real 65 | pypi: 66 | 67 | ```bash 68 | git push origin --tags vAA.BB.CC 69 | twine upload dist/* 70 | ``` 71 | 72 | Congratulations, you published `packet-python`, :raised_hands:! 73 | 74 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | If you require support, please email support@equinixmetal.com, visit the Equinix Metal IRC channel (#equinixmetal on freenode), subscribe to the [Equinix Metal Community Slack](https://slack.equinixmetal.com/) channel or post an issue within this repository. 2 | -------------------------------------------------------------------------------- /packet/BGPConfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class BGPConfig: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.status = data.get("status") 9 | self.deployment_type = data.get("deployment_type") 10 | self.asn = data.get("asn") 11 | self.md5 = data.get("md5") 12 | self.route_object = data.get("route_object") 13 | self.max_prefix = data.get("max_prefix") 14 | self.created_at = data.get("created_at") 15 | self.requested_at = data.get("requested_at") 16 | self.project = data.get("project") 17 | self.sessions = data.get("sessions") 18 | self.ranges = data.get("ranges") 19 | self.href = data.get("href") 20 | 21 | def __str__(self): 22 | return "%s" % self.id 23 | 24 | def __repr__(self): 25 | return "{}: {}".format(self.__class__.__name__, self.id) 26 | -------------------------------------------------------------------------------- /packet/BGPSession.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class BGPSession: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.status = data.get("status") 9 | self.learned_routes = data.get("learned_routes") 10 | self.switch_name = data.get("switch_name") 11 | self.default_route = data.get("default_route") 12 | self.created_at = data.get("created_at") 13 | self.updated_at = data.get("updated_at") 14 | self.device = data.get("device") 15 | self.address_family = data.get("address_family") 16 | self.href = data.get("href") 17 | 18 | def __str__(self): 19 | return "%s" % self.id 20 | 21 | def __repr__(self): 22 | return "{}: {}".format(self.__class__.__name__, self.id) 23 | -------------------------------------------------------------------------------- /packet/Batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Batch: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.error_messages = data.get("error_messages") 9 | self.quantity = data.get("quantity") 10 | self.state = data.get("state") 11 | self.created_at = data.get("created_at") 12 | self.updated_at = data.get("updated_at") 13 | self.devices = data.get("devices") 14 | self.project = data.get("project") 15 | self.state = data.get("state") 16 | self.error_messages = data.get("error_messages") 17 | 18 | def __str__(self): 19 | return "%s" % self.id 20 | 21 | def __repr__(self): 22 | return "{}: {}".format(self.__class__.__name__, self.id) 23 | -------------------------------------------------------------------------------- /packet/Device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | # from .Volume import Volume 4 | from .OperatingSystem import OperatingSystem 5 | 6 | 7 | class Device: 8 | def __init__(self, data, manager): 9 | self.manager = manager 10 | 11 | self.id = data.get("id") 12 | self.short_id = data.get("short_id") 13 | self.hostname = data.get("hostname") 14 | self.description = data.get("description") 15 | self.state = data.get("state") 16 | self.tags = data.get("tags") 17 | self.image_url = data.get("image_url") 18 | self.billing_cycle = data.get("billing_cycle") 19 | self.user = data.get("user") 20 | self.iqn = data.get("iqn") 21 | self.locked = data.get("locked") 22 | self.bonding_mode = data.get("bonding_mode") 23 | self.created_at = data.get("created_at") 24 | self.updated_at = data.get("updated_at") 25 | self.ipxe_script_url = data.get("ipxe_script_url") 26 | self.always_pxe = data.get("always_pxe", False) 27 | self.storage = data.get("storage") 28 | self.customdata = data.get("customdata") 29 | self.operating_system = OperatingSystem(data["operating_system"]) 30 | self.facility = data.get("facility") 31 | self.metro = data.get("metro") 32 | self.project = data.get("project") 33 | self.ssh_keys = data.get("ssh_keys") 34 | self.project_lite = data.get("project_lite") 35 | self.volumes = data.get("volumes") 36 | self.ip_addresses = data.get("ip_addresses") 37 | self.plan = data.get("plan") 38 | self.userdata = data.get("userdata") 39 | self.switch_uuid = data.get("switch_uuid") 40 | self.network_ports = data.get("network_ports") 41 | self.href = data.get("href") 42 | self.spot_instance = data.get("spot_instance", False) 43 | self.hardware_reservation_id = data.get("hardware_reservation_id") 44 | self.spot_price_max = data.get("spot_price_max") 45 | self.termination_time = data.get("termination_time") 46 | self.root_password = data.get("root_password") 47 | self.provisioning_percentage = data.get("provisioning_percentage") 48 | 49 | def update(self): 50 | params = { 51 | "hostname": self.hostname, 52 | "locked": self.locked, 53 | "tags": self.tags, 54 | "description": self.description, 55 | "billing_cycle": self.billing_cycle, 56 | "userdata": self.userdata, 57 | "always_pxe": self.always_pxe, 58 | "ipxe_script_url": self.ipxe_script_url, 59 | "spot_instance": self.spot_instance, 60 | "customdata": self.customdata, 61 | } 62 | 63 | return self.manager.call_api( 64 | "devices/%s" % self.id, type="PATCH", params=params 65 | ) 66 | 67 | def delete(self): 68 | return self.manager.call_api("devices/%s" % self.id, type="DELETE") 69 | 70 | def reinstall(self, operating_system=None, ipxe_script_url=None): 71 | params = {"type": "reinstall"} 72 | if operating_system is not None: 73 | params["operating_system"] = operating_system 74 | if ipxe_script_url is not None: 75 | params["operating_system"] = "custom_ipxe" 76 | params["ipxe_script_url"] = ipxe_script_url 77 | 78 | return self.manager.call_api( 79 | "devices/%s/actions" % self.id, type="POST", params=params 80 | ) 81 | 82 | def power_off(self): 83 | params = {"type": "power_off"} 84 | return self.manager.call_api( 85 | "devices/%s/actions" % self.id, type="POST", params=params 86 | ) 87 | 88 | def power_on(self): 89 | params = {"type": "power_on"} 90 | return self.manager.call_api( 91 | "devices/%s/actions" % self.id, type="POST", params=params 92 | ) 93 | 94 | def reboot(self): 95 | params = {"type": "reboot"} 96 | return self.manager.call_api( 97 | "devices/%s/actions" % self.id, type="POST", params=params 98 | ) 99 | 100 | def rescue(self): 101 | params = {"type": "rescue"} 102 | return self.manager.call_api( 103 | "devices/%s/actions" % self.id, type="POST", params=params 104 | ) 105 | 106 | def ips(self): 107 | return self.manager.list_device_ips(self.id) 108 | 109 | def __str__(self): 110 | return "%s" % self.hostname 111 | 112 | def __repr__(self): 113 | return "{}: {}".format(self.__class__.__name__, self.id) 114 | 115 | def __getitem__(self, item): 116 | return getattr(self, item) 117 | -------------------------------------------------------------------------------- /packet/DeviceBatch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class DeviceBatch: 6 | def __init__(self, data): 7 | self.hostname = data.get("hostname") 8 | self.plan = data.get("plan") 9 | self.operating_system = data.get("operating_system") 10 | self.facility = data.get("facility") 11 | self.metro = data.get("metro") 12 | self.quantity = data.get("quantity") 13 | 14 | def __str__(self): 15 | return "%s" % self.hostname 16 | -------------------------------------------------------------------------------- /packet/Email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Email: 6 | def __init__(self, data, manager): 7 | self.manager = manager 8 | 9 | self.id = data.get("id") 10 | self.address = data.get("address") 11 | self.default = data.get("default") 12 | self.verified = data.get("verified") 13 | 14 | def update(self): 15 | params = {"address": self.address, "default": self.default} 16 | 17 | return self.manager.call_api("emails/%s" % self.id, type="PATCH", params=params) 18 | 19 | def delete(self): 20 | return self.manager.call_api("emails/%s" % self.id, type="DELETE") 21 | 22 | def __str__(self): 23 | return "%s" % self.address 24 | 25 | def __repr__(self): 26 | return "{}: {}".format(self.__class__.__name__, self.id) 27 | -------------------------------------------------------------------------------- /packet/Event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Event: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.type = data.get("type") 9 | self.body = data.get("body") 10 | self.state = data.get("state") 11 | self.created_at = data.get("created_at") 12 | self.modified_by = data.get("modified_by") 13 | self.ip = data.get("ip") 14 | self.interpolated = data.get("interpolated") 15 | 16 | def __str__(self): 17 | return "%s" % self.interpolated 18 | 19 | def __repr__(self): 20 | return "{}: {}".format(self.__class__.__name__, self.id) 21 | -------------------------------------------------------------------------------- /packet/Facility.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Facility: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.code = data.get("code") 9 | self.name = data.get("name") 10 | self.features = data.get("features") 11 | self.address = data.get("address") 12 | 13 | def __str__(self): 14 | return "%s" % self.code 15 | 16 | def __repr__(self): 17 | return "{}: {}".format(self.__class__.__name__, self.id) 18 | -------------------------------------------------------------------------------- /packet/HardwareReservation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | from .Plan import Plan 5 | from .Project import Project 6 | from .Device import Device 7 | 8 | 9 | class HardwareReservation: 10 | def __init__(self, data, manager): 11 | self.manager = manager 12 | 13 | self.id = data.get("id") 14 | self.created_at = data.get("created_at") 15 | self.billing_cycle = data.get("billing_cycle") 16 | self.created_at = data.get("created_at") 17 | self.short_id = data.get("short_id") 18 | self.intervals = data.get("intervals") 19 | self.current_period = data.get("current_period") 20 | self.started_at = data.get("started_at") 21 | self.custom_rate = data.get("custom_rate") 22 | self.remove_at = data.get("remove_at") 23 | self.project = data.get("project") 24 | # self.facility = data.get("facility") 25 | self.device = data.get("device") 26 | self.provisionable = data.get("provisionable") 27 | self.spare = data.get("spare") 28 | self.need_of_service = data.get("need_of_service") 29 | self.plan = Plan(data.get("plan")) 30 | self.switch_uuid = data.get("switch_uuid") 31 | 32 | try: 33 | project_data = self.manager.call_api(data["project"]["href"], type="GET") 34 | self.project = Project(project_data, self.manager) 35 | except (KeyError, IndexError): 36 | self.attached_to = None 37 | 38 | # endpoint is not working yet 39 | # try: 40 | # facility_data = self.manager.call_api( 41 | # data["facility"]["href"], type="GET" 42 | # ) 43 | # self.project = Facility(facility_data, self.manager) 44 | # except (KeyError, IndexError): 45 | # self.attached_to = None 46 | 47 | try: 48 | device_data = self.manager.call_api(data["device"]["href"], type="GET") 49 | self.device = Device(device_data, self.manager) 50 | except (KeyError, IndexError, TypeError): 51 | self.attached_to = None 52 | 53 | def __str__(self): 54 | return "%s" % self.id 55 | 56 | def __repr__(self): 57 | return "{}: {}".format(self.__class__.__name__, self.id) 58 | 59 | def __getitem__(self, item): 60 | return getattr(self, item) 61 | -------------------------------------------------------------------------------- /packet/IPAddress.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | from .Facility import Facility 5 | from .Metro import Metro 6 | 7 | 8 | class IPAddress: 9 | def __init__(self, data, manager): 10 | self.manager = manager 11 | 12 | self.id = data.get("id") 13 | self.address_family = data.get("address_family") 14 | self.netmask = data.get("netmask") 15 | self.created_at = data.get("created_at") 16 | self.details = data.get("details") 17 | self.tags = data.get("tags") 18 | self.public = data.get("public") 19 | self.cidr = data.get("cidr") 20 | self.management = data.get("management") 21 | self.enabled = data.get("enabled") 22 | self.global_ip = data.get("global_ip") 23 | self.customdata = data.get("customdata") 24 | self.project = data.get("project") 25 | self.project_lite = data.get("project_lite") 26 | self.details = data.get("details") 27 | self.assigned_to = data.get("assigned_to") 28 | self.interface = data.get("interface") 29 | self.network = data.get("network") 30 | self.address = data.get("address") 31 | self.gateway = data.get("gateway") 32 | 33 | facility = data.get("facility") 34 | self.facility = Facility(facility) if facility else None 35 | metro = data.get("metro") 36 | self.metro = Metro(metro) if metro else None 37 | 38 | def delete(self): 39 | return self.manager.call_api("ips/%s" % self.id, type="DELETE") 40 | 41 | def __str__(self): 42 | return "%s" % self.code 43 | 44 | def __repr__(self): 45 | return "{}: {}".format(self.__class__.__name__, self.id) 46 | -------------------------------------------------------------------------------- /packet/Manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | from packet.Vlan import Vlan 4 | from .baseapi import BaseAPI 5 | from .baseapi import Error as PacketError 6 | from .baseapi import ResponseError 7 | from .Batch import Batch 8 | from .Plan import Plan 9 | from .Device import Device 10 | from .SSHKey import SSHKey 11 | from .Project import Project 12 | from .Facility import Facility 13 | from .Metro import Metro 14 | from .OperatingSystem import OperatingSystem 15 | from .Volume import Volume 16 | from .BGPConfig import BGPConfig 17 | from .BGPSession import BGPSession 18 | from .IPAddress import IPAddress 19 | from .HardwareReservation import HardwareReservation 20 | from .Snapshot import Snapshot 21 | from .Organization import Organization 22 | from .Email import Email 23 | from .Event import Event 24 | from .Provider import Provider 25 | 26 | 27 | class Manager(BaseAPI): 28 | def __init__(self, auth_token, consumer_token=None): 29 | super(Manager, self).__init__(auth_token, consumer_token) 30 | 31 | def call_api(self, method, type="GET", params=None): 32 | return super(Manager, self).call_api(method, type, params) 33 | 34 | def get_user(self): 35 | return self.call_api("user") 36 | 37 | def list_facilities(self, params={}): 38 | data = self.call_api("facilities", params=params) 39 | facilities = list() 40 | for jsoned in data["facilities"]: 41 | facility = Facility(jsoned) 42 | facilities.append(facility) 43 | return facilities 44 | 45 | def list_metros(self, params={}): 46 | data = self.call_api("locations/metros", params=params) 47 | metros = list() 48 | for jsoned in data["metros"]: 49 | metro = Metro(jsoned) 50 | metros.append(metro) 51 | return metros 52 | 53 | def list_plans(self, params={}): 54 | data = self.call_api("plans", params=params) 55 | plans = list() 56 | for jsoned in data["plans"]: 57 | plan = Plan(jsoned) 58 | plans.append(plan) 59 | return plans 60 | 61 | def list_operating_systems(self, params={}): 62 | data = self.call_api("operating-systems", params=params) 63 | oss = list() 64 | for jsoned in data["operating_systems"]: 65 | os = OperatingSystem(jsoned) 66 | oss.append(os) 67 | return oss 68 | 69 | def list_projects(self, params={}): 70 | data = self.call_api("projects", params=params) 71 | self.total = data["meta"]["total"] 72 | projects = list() 73 | for jsoned in data["projects"]: 74 | project = Project(jsoned, self) 75 | projects.append(project) 76 | return projects 77 | 78 | def get_project(self, project_id): 79 | data = self.call_api("projects/%s" % project_id) 80 | return Project(data, self) 81 | 82 | def create_project(self, name): 83 | params = {"name": name} 84 | data = self.call_api("projects", type="POST", params=params) 85 | return Project(data, self) 86 | 87 | def list_hardware_reservations(self, project_id, params={}): 88 | data = self.call_api( 89 | "projects/%s/hardware-reservations" % project_id, params=params 90 | ) 91 | hardware_reservations = list() 92 | for jsoned in data["hardware_reservations"]: 93 | hardware_reservation = HardwareReservation(jsoned, self) 94 | hardware_reservations.append(hardware_reservation) 95 | return hardware_reservations 96 | 97 | def get_hardware_reservation(self, hardware_reservation_id): 98 | data = self.call_api("hardware-reservations/%s" % hardware_reservation_id) 99 | return HardwareReservation(data, self) 100 | 101 | def list_devices(self, project_id, params={}): 102 | data = self.call_api("projects/%s/devices" % project_id, params=params) 103 | devices = list() 104 | for jsoned in data["devices"]: 105 | device = Device(jsoned, self) 106 | devices.append(device) 107 | return devices 108 | 109 | def list_all_devices(self, project_id): 110 | raw_devices = list() 111 | page = 1 112 | while True: 113 | paginate = {"page": page} 114 | data = self.call_api("projects/%s/devices" % project_id, params=paginate) 115 | next = self.meta["next"] 116 | raw_devices.extend(data["devices"]) 117 | if next is None: 118 | break 119 | else: 120 | page += 1 121 | 122 | all_devices = list() 123 | for raw_device in raw_devices: 124 | device = Device(raw_device, self) 125 | all_devices.append(device) 126 | return all_devices 127 | 128 | def create_device( 129 | self, 130 | project_id, 131 | hostname, 132 | plan, 133 | facility="", 134 | operating_system="", 135 | always_pxe=False, 136 | billing_cycle="hourly", 137 | features={}, 138 | ipxe_script_url="", 139 | locked=False, 140 | project_ssh_keys=[], 141 | public_ipv4_subnet_size=31, 142 | spot_instance=False, 143 | spot_price_max=-1, 144 | tags={}, 145 | termination_time=None, 146 | user_ssh_keys=[], 147 | userdata="", 148 | hardware_reservation_id="", 149 | storage={}, 150 | customdata={}, 151 | metro="", 152 | ): 153 | 154 | params = { 155 | "billing_cycle": billing_cycle, 156 | "features": features, 157 | "hostname": hostname, 158 | "locked": locked, 159 | "operating_system": operating_system, 160 | "plan": plan, 161 | "project_id": project_id, 162 | "public_ipv4_subnet_size": public_ipv4_subnet_size, 163 | "project_ssh_keys": project_ssh_keys, 164 | "tags": tags, 165 | "user_ssh_keys": user_ssh_keys, 166 | "userdata": userdata, 167 | } 168 | 169 | if metro != "": 170 | params["metro"] = metro 171 | if facility != "": 172 | params["facility"] = facility 173 | if hardware_reservation_id != "": 174 | params["hardware_reservation_id"] = hardware_reservation_id 175 | if storage: 176 | params["storage"] = storage 177 | if customdata: 178 | params["customdata"] = customdata 179 | if ipxe_script_url != "": 180 | params["always_pxe"] = always_pxe 181 | params["ipxe_script_url"] = ipxe_script_url 182 | params["operating_system"] = "custom_ipxe" 183 | if spot_instance: 184 | params["spot_instance"] = spot_instance 185 | params["spot_price_max"] = spot_price_max 186 | params["termination_time"] = termination_time 187 | data = self.call_api( 188 | "projects/%s/devices" % project_id, type="POST", params=params 189 | ) 190 | return Device(data, self) 191 | 192 | def get_device(self, device_id): 193 | data = self.call_api("devices/%s" % device_id) 194 | return Device(data, self) 195 | 196 | def list_ssh_keys(self, params={}): 197 | data = self.call_api("ssh-keys", params=params) 198 | ssh_keys = list() 199 | for jsoned in data["ssh_keys"]: 200 | ssh_key = SSHKey(jsoned, self) 201 | ssh_keys.append(ssh_key) 202 | return ssh_keys 203 | 204 | def get_ssh_key(self, ssh_key_id): 205 | data = self.call_api("ssh-keys/%s" % ssh_key_id) 206 | return SSHKey(data, self) 207 | 208 | def create_ssh_key(self, label, public_key): 209 | params = {"key": public_key, "label": label} 210 | data = self.call_api("ssh-keys", type="POST", params=params) 211 | return SSHKey(data, self) 212 | 213 | def create_project_ssh_key(self, project_id, label, public_key): 214 | """ 215 | Successfully creating an SSH key with a Project API Token results 216 | in a 404 from the API. If we get a 404, we try the request again. 217 | 218 | If the request actually failed with a 404, we will get another 404 219 | which we raise. 220 | 221 | If the request actually succeeded, we will get a 422. In this case, 222 | we will try to list all the keys and find the SSHKey we just 223 | received. 224 | 225 | Customer Report Reference: TUVD-0107-UIKB 226 | """ 227 | 228 | def issue_req(): 229 | try: 230 | params = {"key": public_key, "label": label} 231 | data = self.call_api( 232 | "projects/%s/ssh-keys" % project_id, type="POST", params=params 233 | ) 234 | return SSHKey(data, self) 235 | except ResponseError as e: 236 | if e.response.status_code == 422: 237 | # Try to pluck the SSH key from the listing API 238 | keys = [ 239 | key 240 | for key in self.list_ssh_keys() 241 | if key.key.strip() == public_key.strip() 242 | ] 243 | if len(keys) == 1: 244 | return keys.pop() 245 | raise 246 | 247 | try: 248 | return issue_req() 249 | except ResponseError as e: 250 | if e.response.status_code == 404: 251 | return issue_req() 252 | else: 253 | raise 254 | 255 | def list_volumes(self, project_id, params={}): 256 | params["include"] = "facility,attachments.device" 257 | data = self.call_api("projects/%s/storage" % project_id, params=params) 258 | volumes = list() 259 | for jsoned in data["volumes"]: 260 | volume = Volume(jsoned, self) 261 | volumes.append(volume) 262 | return volumes 263 | 264 | def create_volume( 265 | self, 266 | project_id, 267 | description, 268 | plan, 269 | size, 270 | facility, 271 | snapshot_count=0, 272 | snapshot_frequency=None, 273 | ): 274 | params = { 275 | "description": description, 276 | "plan": plan, 277 | "size": size, 278 | "facility": facility, 279 | } 280 | 281 | if snapshot_count > 0 and snapshot_frequency is not None: 282 | params["snapshot_policies"] = { 283 | "snapshot_count": snapshot_count, 284 | "snapshot_frequency": snapshot_frequency, 285 | } 286 | 287 | data = self.call_api( 288 | "projects/%s/storage?include=facility" % project_id, 289 | type="POST", 290 | params=params, 291 | ) 292 | 293 | return Volume(data, self) 294 | 295 | def get_volume(self, volume_id): 296 | params = {"include": "facility,attachments.device"} 297 | data = self.call_api("storage/%s" % volume_id, params=params) 298 | return Volume(data, self) 299 | 300 | def get_capacity(self, legacy=None): 301 | """Get capacity of all facilities. 302 | 303 | :param legacy: Indicate set of server types to include in response 304 | 305 | Validation of `legacy` is left to the packet api to avoid going out 306 | of date if any new value is introduced. 307 | The currently known values are: 308 | - only (current default, will be switched "soon") 309 | - include 310 | - exclude (soon to be default) 311 | """ 312 | params = None 313 | if legacy: 314 | params = {"legacy": legacy} 315 | 316 | return self.call_api("/capacity", params=params)["capacity"] 317 | 318 | # servers is a list of tuples of facility, plan, and quantity. 319 | def validate_capacity(self, servers): 320 | params = {"servers": []} 321 | for server in servers: 322 | params["servers"].append( 323 | {"facility": server[0], "plan": server[1], "quantity": server[2]} 324 | ) 325 | 326 | try: 327 | data = self.call_api("/capacity", "POST", params) 328 | return all(s["available"] for s in data["servers"]) 329 | except PacketError as e: # pragma: no cover 330 | if e.args[0] == "Error 503: Service Unavailable": 331 | return False 332 | else: 333 | raise e 334 | 335 | # servers is a list of tuples of metro, plan, and quantity. 336 | def validate_metro_capacity(self, servers): 337 | params = {"servers": []} 338 | for server in servers: 339 | params["servers"].append( 340 | {"metro": server[0], "plan": server[1], "quantity": server[2]} 341 | ) 342 | 343 | try: 344 | data = self.call_api("/capacity/metros", "POST", params) 345 | return all(s["available"] for s in data["servers"]) 346 | except PacketError as e: # pragma: no cover 347 | if e.args[0] == "Error 503: Service Unavailable": 348 | return False 349 | else: 350 | raise e 351 | 352 | def get_spot_market_prices(self, params={}): 353 | data = self.call_api("/market/spot/prices", params=params) 354 | return data["spot_market_prices"] 355 | 356 | # BGP Config 357 | def get_bgp_config(self, project_id): 358 | data = self.call_api("projects/%s/bgp-config" % project_id) 359 | return BGPConfig(data) 360 | 361 | def enable_project_bgp_config( 362 | self, project_id, asn, deployment_type, md5=None, use_case=None 363 | ): 364 | params = { 365 | "deployment_type": deployment_type, 366 | "asn": asn, 367 | "md5": md5, 368 | "use_case": use_case, 369 | } 370 | self.call_api( 371 | "/projects/%s/bgp-configs" % project_id, type="POST", params=params 372 | ) 373 | 374 | # BGP Session 375 | def get_bgp_sessions(self, device_id, params={}): 376 | data = self.call_api( 377 | "/devices/%s/bgp/sessions" % device_id, type="GET", params=params 378 | ) 379 | bgp_sessions = list() 380 | for jsoned in data["bgp_sessions"]: 381 | bpg_session = BGPSession(jsoned) 382 | bgp_sessions.append(bpg_session) 383 | return bgp_sessions 384 | 385 | def create_bgp_session(self, device_id, address_family): 386 | data = self.call_api( 387 | "/devices/%s/bgp/sessions" % device_id, 388 | type="POST", 389 | params={"address_family": address_family}, 390 | ) 391 | return BGPSession(data) 392 | 393 | # IP operations 394 | def list_device_ips(self, device_id): 395 | data = self.call_api("devices/%s/ips" % device_id, type="GET") 396 | ips = list() 397 | for jsoned in data["ip_addresses"]: 398 | ip = IPAddress(jsoned, self) 399 | ips.append(ip) 400 | return ips 401 | 402 | def get_ip(self, ip_id): 403 | data = self.call_api("ips/%s" % ip_id) 404 | return IPAddress(data, self) 405 | 406 | def delete_ip(self, ip_id): 407 | self.call_api("ips/%s" % ip_id, type="DELETE") 408 | 409 | def list_project_ips(self, project_id, params={}): 410 | data = self.call_api("projects/%s/ips" % project_id, type="GET", params=params) 411 | ips = list() 412 | for jsoned in data["ip_addresses"]: 413 | ip = IPAddress(jsoned, self) 414 | ips.append(ip) 415 | return ips 416 | 417 | def get_available_ip_subnets(self, ip_id, cidr): 418 | data = self.call_api( 419 | "/ips/%s/available" % ip_id, type="GET", params="cidr=%s" % cidr 420 | ) 421 | return data 422 | 423 | def create_device_ip(self, device_id, address, manageable=True, customdata=None): 424 | params = { 425 | "address": address, 426 | "manageable": manageable, 427 | "customdata": customdata, 428 | } 429 | 430 | data = self.call_api("/devices/%s/ips" % device_id, params=params, type="POST") 431 | return IPAddress(data, self) 432 | 433 | def reserve_ip_address( 434 | self, 435 | project_id, 436 | type, 437 | quantity, 438 | facility="", 439 | details=None, 440 | comments=None, 441 | tags=list(), 442 | metro="", 443 | ): 444 | request = { 445 | "type": type, 446 | "quantity": quantity, 447 | "details": details, 448 | "comments": comments, 449 | "tags": tags, 450 | } 451 | 452 | if facility != "": 453 | request["facility"] = facility 454 | if metro != "": 455 | request["metro"] = metro 456 | data = self.call_api( 457 | "/projects/%s/ips" % project_id, params=request, type="POST" 458 | ) 459 | return IPAddress(data, self) 460 | 461 | # Batches 462 | def create_batch(self, project_id, params): 463 | param = {"batches": params} 464 | data = self.call_api( 465 | "/projects/%s/devices/batch" % project_id, type="POST", params=param 466 | ) 467 | batches = list() 468 | for b in data["batches"]: 469 | batch = Batch(b) 470 | batches.append(batch) 471 | return batches 472 | 473 | def list_batches(self, project_id, params=None): 474 | data = self.call_api( 475 | "/projects/%s/batches" % project_id, type="GET", params=params 476 | ) 477 | batches = list() 478 | for b in data["batches"]: 479 | batch = Batch(b) 480 | batches.append(batch) 481 | return batches 482 | 483 | def delete_batch(self, batch_id, remove_associated_instances=False): 484 | self.call_api( 485 | "/batches/%s" % batch_id, type="DELETE", params=remove_associated_instances 486 | ) 487 | 488 | # Snapshots 489 | def get_snapshots(self, volume_id, params=None): 490 | data = self.call_api( 491 | "storage/%s/snapshots" % volume_id, type="GET", params=params 492 | ) 493 | snapshots = list() 494 | for ss in data["snapshots"]: 495 | snapshot = Snapshot(ss) 496 | snapshots.append(snapshot) 497 | 498 | return snapshots 499 | 500 | def restore_volume(self, volume_id, restore_point): 501 | params = {"restore_point": restore_point} 502 | self.call_api("storage/%s/restore" % volume_id, type="POST", params=params) 503 | 504 | # Organization 505 | def list_organizations(self, params=None): 506 | data = self.call_api("organizations", type="GET", params=params) 507 | orgs = list() 508 | for org in data["organizations"]: 509 | o = Organization(org) 510 | orgs.append(o) 511 | 512 | return orgs 513 | 514 | def get_organization(self, org_id, params=None): 515 | data = self.call_api("organizations/%s" % org_id, type="GET", params=params) 516 | return Organization(data) 517 | 518 | def list_organization_projects(self, org_id, params=None): 519 | data = self.call_api( 520 | "organizations/%s/projects" % org_id, type="GET", params=params 521 | ) 522 | projects = list() 523 | for p in data["projects"]: 524 | projects.append(Project(p, self)) 525 | 526 | return projects 527 | 528 | def list_organization_devices(self, org_id, params=None): 529 | data = self.call_api( 530 | "organizations/%s/devices" % org_id, type="GET", params=params 531 | ) 532 | devices = list() 533 | for d in data["devices"]: 534 | devices.append(Device(d, self)) 535 | 536 | return devices 537 | 538 | def create_organization_project( 539 | self, org_id, name, payment_method_id=None, customdata=None 540 | ): 541 | params = { 542 | "name": name, 543 | "payment_method_id": payment_method_id, 544 | "customdata": customdata, 545 | } 546 | data = self.call_api( 547 | "organizations/%s/projects" % org_id, type="POST", params=params 548 | ) 549 | return Project(data, self) 550 | 551 | # Email 552 | def add_email(self, address, default=False): 553 | params = {"address": address, "default": default} 554 | data = self.call_api("emails", type="POST", params=params) 555 | return Email(data, self) 556 | 557 | def get_email(self, email_id): 558 | data = self.call_api("emails/%s" % email_id) 559 | return Email(data, self) 560 | 561 | # Event 562 | def list_events(self, params=None): 563 | data = self.call_api("events", type="GET", params=params) 564 | events = list() 565 | for e in data["events"]: 566 | events.append(Event(e)) 567 | 568 | return events 569 | 570 | def get_event(self, event_id): 571 | data = self.call_api("events/%s" % event_id) 572 | return Event(data) 573 | 574 | def list_device_events(self, device_id, params=None): 575 | data = self.call_api("devices/%s/events" % device_id, type="GET", params=params) 576 | events = list() 577 | for e in data["events"]: 578 | events.append(Event(e)) 579 | 580 | return events 581 | 582 | def list_project_events(self, project_id, params=None): 583 | data = self.call_api( 584 | "projects/%s/events" % project_id, type="GET", params=params 585 | ) 586 | events = list() 587 | for e in data["events"]: 588 | events.append(Event(e)) 589 | 590 | return events 591 | 592 | def get_volume_events(self, volume_id, params=None): 593 | data = self.call_api("volumes/%s/events" % volume_id, type="GET", params=params) 594 | events = list() 595 | for e in data["events"]: 596 | events.append(Event(e)) 597 | 598 | return events 599 | 600 | # vlan operations 601 | def list_vlans(self, project_id, params=None): 602 | data = self.call_api( 603 | "projects/%s/virtual-networks" % project_id, type="GET", params=params 604 | ) 605 | vlans = list() 606 | for vlan in data["virtual_networks"]: 607 | vlans.append(Vlan(vlan, self)) 608 | 609 | return vlans 610 | 611 | def create_vlan( 612 | self, project_id, facility="", vxlan=None, vlan=None, description=None, metro="" 613 | ): 614 | params = { 615 | "project_id": project_id, 616 | "vxlan": vxlan, 617 | "vlan": vlan, 618 | "description": description, 619 | } 620 | if facility != "": 621 | params["facility"] = facility 622 | if metro != "": 623 | params["metro"] = metro 624 | data = self.call_api( 625 | "projects/%s/virtual-networks" % project_id, type="POST", params=params 626 | ) 627 | return Vlan(data, self) 628 | 629 | def assign_port(self, port_id, vlan_id): 630 | params = {"vnid": vlan_id} 631 | self.call_api("ports/%s/assign" % port_id, type="POST", params=params) 632 | 633 | def remove_port(self, port_id, vlan_id): 634 | params = {"vnid": vlan_id} 635 | self.call_api("ports/%s/unassign" % port_id, type="POST", params=params) 636 | 637 | def disbond_ports(self, port_id, bulk_disable): 638 | params = {"bulk_disable": bulk_disable} 639 | self.call_api("ports/%s/disbond" % port_id, type="POST", params=params) 640 | 641 | def bond_ports(self, port_id, bulk_disable): 642 | params = {"bulk_disable": bulk_disable} 643 | self.call_api("ports/%s/bond" % port_id, type="POST", params=params) 644 | 645 | def convert_layer_2(self, port_id, vlan_id): 646 | params = {"vnid": vlan_id} 647 | self.call_api("ports/%s/convert/layer-2" % port_id, type="POST", params=params) 648 | 649 | def convert_layer_3(self, port_id, request_ips): 650 | params = {"request_ips": request_ips} 651 | self.call_api("ports/%s/convert/layer-3" % port_id, type="POST", params=params) 652 | 653 | def assign_native_vlan(self, port_id, vnid): 654 | params = {"vnid": vnid} 655 | self.call_api("ports/%s/native-vlan" % port_id, type="POST", params=params) 656 | 657 | def remove_native_vlan(self, port_id): 658 | self.call_api("ports/%s/native-vlan" % port_id, type="DELETE") 659 | 660 | def get_vpn_configuration(self, facilityCode): 661 | params = {"code": facilityCode} 662 | data = self.call_api("user/vpn", type="GET", params=params) 663 | return data 664 | 665 | def turn_on_vpn(self): 666 | return self.call_api("user/vpn", type="POST") 667 | 668 | def turn_off_vpn(self): 669 | return self.call_api("user/vpn", type="DELETE") 670 | 671 | # Packet connect 672 | def create_packet_connections(self, params): 673 | body = { 674 | "name": params["name"], 675 | "project_id": params["project_id"], 676 | "provider_id": params["provider_id"], 677 | "provider_payload": params["provider_payload"], 678 | "port_speed": params["port_speed"], 679 | "vlan": params["vlan"], 680 | } 681 | for opt in ["tags", "description", "facility", "metro"]: 682 | if opt in params: 683 | body[opt] = params[opt] 684 | 685 | data = self.call_api("/packet-connect/connections", type="POST", params=body) 686 | return data 687 | 688 | def get_packet_connection(self, connection_id): 689 | data = self.call_api("/packet-connect/connections/%s" % connection_id) 690 | return data 691 | 692 | def delete_packet_connection(self, connection_id): 693 | data = self.call_api( 694 | "/packet-connect/connections/%s" % connection_id, type="DELETE" 695 | ) 696 | return data 697 | 698 | def provision_packet_connection(self, connection_id): 699 | data = self.call_api( 700 | "/packet-connect/connections/{id}/provision" % connection_id, type="POST" 701 | ) 702 | return data 703 | 704 | def deprovision_packet_connection(self, connection_id, delete): 705 | params = {"delete": delete} 706 | data = self.call_api( 707 | "/packet-connect/connections/{id}/deprovision" % connection_id, 708 | type="POST", 709 | params=params, 710 | ) 711 | return data 712 | 713 | def list_packet_connect_providers(self): 714 | data = self.call_api("/packet-connect/providers", type="GET") 715 | providers = list() 716 | for p in data["providers"]: 717 | providers.append(Provider(p)) 718 | return providers 719 | 720 | def get_packet_connect_provider(self, provider_id): 721 | data = self.call_api("/packet-connect/providers/%s" % provider_id, type="GET") 722 | return Provider(data) 723 | -------------------------------------------------------------------------------- /packet/Metro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Metro: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.code = data.get("code") 9 | self.name = data.get("name") 10 | self.country = data.get("country") 11 | 12 | def __str__(self): 13 | return "%s" % self.code 14 | 15 | def __repr__(self): 16 | return "{}: {}".format(self.__class__.__name__, self.id) 17 | -------------------------------------------------------------------------------- /packet/OperatingSystem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class OperatingSystem(object): 6 | def __init__(self, data): 7 | self.slug = data.get("slug") 8 | self.name = data.get("name") 9 | self.distro = data.get("distro") 10 | self.version = data.get("version") 11 | self.provisionable_on = data.get("provisionable_on") 12 | 13 | def __str__(self): 14 | return "%s %s %s %s" % (self.slug, self.name, self.distro, self.version) 15 | 16 | def __repr__(self): 17 | return "{}: {}".format(self.__class__.__name__, self.slug) 18 | -------------------------------------------------------------------------------- /packet/Organization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Organization: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.name = data.get("name") 9 | self.description = data.get("description") 10 | self.website = data.get("website") 11 | self.twitter = data.get("twitter") 12 | self.created_at = data.get("created_at") 13 | self.updated_at = data.get("updated_at") 14 | self.tax_id = data.get("tax_id") 15 | self.main_phone = data.get("main_phone") 16 | self.billing_phone = data.get("billing_phone") 17 | self.credit_amount = data.get("credit_amount") 18 | self.personal = data.get("personal") 19 | self.customdata = data.get("customdata") 20 | self.attn = data.get("attn") 21 | self.purchase_order = data.get("purchase_order") 22 | self.billing_name = data.get("billing_name") 23 | self.enforce_2fa = data.get("enforce_2fa") 24 | self.enforce_2fa_at = data.get("enforce_2fa_at") 25 | self.short_id = data.get("short_id") 26 | self.account_id = data.get("account_id") 27 | self.enabled_features = data.get("enabled_features") 28 | self.maintenance_email = data.get("maintenance_email") 29 | self.abuse_email = data.get("abuse_email") 30 | self.address = data.get("address") 31 | self.billing_address = data.get("billing_address") 32 | self.account_manager = data.get("account_manager") 33 | self.logo = data.get("logo") 34 | self.logo_thumb = data.get("logo_thumb") 35 | self.projects = data.get("projects") 36 | self.plan = data.get("plan") 37 | self.monthly_spend = data.get("monthly_spend") 38 | self.current_user_abilities = data.get("current_user_abilities") 39 | self.href = data.get("href") 40 | 41 | def __str__(self): 42 | return "%s" % self.id 43 | 44 | def __repr__(self): 45 | return "{}: {}".format(self.__class__.__name__, self.id) 46 | -------------------------------------------------------------------------------- /packet/Plan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Plan: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.name = data.get("name") 9 | self.slug = data.get("slug") 10 | self.line = data.get("line") 11 | self.pricing = data.get("pricing") 12 | self.specs = data.get("specs") 13 | self.description = data.get("description") 14 | self.available_in = data.get("available_in") 15 | 16 | def __str__(self): 17 | return "%s" % self.slug 18 | 19 | def __repr__(self): 20 | return "{}: {}".format(self.__class__.__name__, self.id) 21 | -------------------------------------------------------------------------------- /packet/Project.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Project: 6 | def __init__(self, data, manager): 7 | self.manager = manager 8 | 9 | self.id = data.get("id") 10 | self.name = data.get("name") 11 | self.payment_method = data.get("payment_method", []) 12 | self.max_projects = data.get("max_devices") 13 | self.created_at = data.get("created_at") 14 | self.updated_at = data.get("updated_at") 15 | self.devices = data.get("devices") 16 | self.invitations = data.get("invitations") 17 | self.memberships = data.get("memberships") 18 | self.members = data.get("members") 19 | self.ssh_keys = data.get("ssh_keys") 20 | 21 | def update(self): 22 | params = {"name": self.name} 23 | 24 | return self.manager.call_api( 25 | "projects/%s" % self.id, type="PATCH", params=params 26 | ) 27 | 28 | def delete(self): 29 | return self.manager.call_api("projects/%s" % self.id, type="DELETE") 30 | 31 | def __str__(self): 32 | return "%s" % self.name 33 | 34 | def __repr__(self): 35 | return "{}: {}".format(self.__class__.__name__, self.id) 36 | -------------------------------------------------------------------------------- /packet/Provider.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Provider: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.name = data.get("name") 9 | self.status = data.get("status") 10 | self.type = data.get("type") 11 | self.public = data.get("public") 12 | 13 | def __str__(self): 14 | return "%s" % self.id 15 | 16 | def __repr__(self): 17 | return "{}: {}".format(self.__class__.__name__, self.id) 18 | -------------------------------------------------------------------------------- /packet/SSHKey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class SSHKey: 6 | def __init__(self, data, manager): 7 | self.manager = manager 8 | 9 | self.id = data.get("id") 10 | self.key = data.get("key") 11 | self.label = data.get("label") 12 | self.fingerprint = data.get("fingerprint") 13 | self.href = data.get("href") 14 | self.created_at = data.get("created_at") 15 | self.updated_at = data.get("updated_at") 16 | 17 | try: 18 | self.owner = data["owner"]["href"] 19 | except (KeyError, TypeError): 20 | self.owner = None 21 | 22 | def update(self): 23 | params = {"label": self.label, "key": self.key} 24 | 25 | return self.manager.call_api( 26 | "ssh-keys/%s" % self.id, type="PATCH", params=params 27 | ) 28 | 29 | def delete(self): 30 | return self.manager.call_api("ssh-keys/%s" % self.id, type="DELETE") 31 | 32 | def __str__(self): 33 | return "%s %s" % (self.id, self.label) 34 | 35 | def __repr__(self): 36 | return "{}: {}".format(self.__class__.__name__, self.id) 37 | -------------------------------------------------------------------------------- /packet/Snapshot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | 5 | class Snapshot: 6 | def __init__(self, data): 7 | self.id = data.get("id") 8 | self.status = data.get("status") 9 | self.timestamp = data.get("timestamp") 10 | self.created_at = data.get("created_at") 11 | self.volume = data.get("volume") 12 | 13 | def __str__(self): 14 | return "%s" % self.name 15 | 16 | def __repr__(self): 17 | return "{}: {}".format(self.__class__.__name__, self.id) 18 | -------------------------------------------------------------------------------- /packet/Vlan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | from packet import Project 4 | from .Facility import Facility 5 | from .Metro import Metro 6 | 7 | 8 | class Vlan: 9 | def __init__(self, data, manager): 10 | self.manager = manager 11 | if data is None: 12 | return 13 | 14 | self.id = data.get("id") 15 | self.description = data.get("description") 16 | self.vxlan = data.get("vxlan") 17 | self.internet_gateway = data.get("internet_gateway") 18 | self.facility_code = data.get("facility_code") 19 | self.metro_code = data.get("metro_code") 20 | self.created_at = data.get("created_at") 21 | facility = data.get("facility") 22 | self.facility = Facility(facility) if facility else None 23 | metro = data.get("metro") 24 | self.metro = Metro(metro) if metro else None 25 | 26 | try: 27 | project_data = self.manager.call_api( 28 | data["assigned_to"]["href"], type="GET" 29 | ) 30 | self.assigned_to = Project(project_data, self.manager) 31 | except (KeyError, IndexError): 32 | self.attached_to = None 33 | 34 | def get(self): 35 | return self.manager.call_api("virtual-networks/%s" % self.id, type="GET") 36 | 37 | def delete(self): 38 | return self.manager.call_api("virtual-networks/%s" % self.id, type="DELETE") 39 | 40 | def create_internet_gateway(self, ip_reservation_length): 41 | """:param ip_reservation_length: (required) number of IP addresses possible 8 or 16""" 42 | params = {"length": ip_reservation_length} 43 | return self.manager.call_api( 44 | "/virtual-networks/%s/internet-gateways" % self.id, 45 | type="POST", 46 | params=params, 47 | ) 48 | 49 | def assign_native_vlan(self, port_id): 50 | params = {"vnid": self.id} 51 | return self.manager.call_api( 52 | "/ports/%s/native-vlan" % port_id, type="POST", params=params 53 | ) 54 | 55 | def remove_native_vlan(self, port_id): 56 | return self.manager.call_api("/ports/%s/native-vlan" % port_id, type="DELETE") 57 | 58 | def __str__(self): 59 | return "%s" % self.id 60 | 61 | def __repr__(self): 62 | return "{}: {}".format(self.__class__.__name__, self.id) 63 | -------------------------------------------------------------------------------- /packet/Volume.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | from .Plan import Plan 5 | from .Facility import Facility 6 | 7 | 8 | class Volume: 9 | def __init__(self, data, manager): 10 | self.manager = manager 11 | if data is None: 12 | return 13 | 14 | self.id = data.get("id") 15 | self.name = data.get("name") 16 | self.description = data.get("description") 17 | self.size = data.get("size") 18 | self.state = data.get("state") 19 | self.locked = data.get("locked") 20 | self.billing_cycle = data.get("billing_cycle") 21 | self.created_at = data.get("created_at") 22 | self.updated_at = data.get("updated_at") 23 | self.attachments = data.get("attachments") 24 | 25 | self.plan = Plan(data.get("plan")) 26 | self.facility = Facility(data.get("facility")) 27 | try: 28 | self.attached_to = data["attachments"][0]["device"]["id"] 29 | except (KeyError, IndexError): 30 | self.attached_to = None 31 | 32 | def update(self): 33 | params = { 34 | "description": self.description, 35 | "size": self.size, 36 | "plan": self.plan.slug, 37 | "locked": self.locked, 38 | } 39 | 40 | return self.manager.call_api( 41 | "storage/%s" % self.id, type="PATCH", params=params 42 | ) 43 | 44 | def delete(self): 45 | return self.manager.call_api("storage/%s" % self.id, type="DELETE") 46 | 47 | def attach(self, device_id): 48 | params = {"device_id": device_id} 49 | return self.manager.call_api( 50 | "storage/%s/attachments" % self.id, type="POST", params=params 51 | ) 52 | 53 | def detach(self): 54 | for attachment in self.attachments: 55 | return self.manager.call_api(attachment["href"], type="DELETE") 56 | 57 | def list_snapshots(self, params={}): 58 | data = self.manager.call_api("storage/%s/snapshots" % (self.id)) 59 | snapshots = list() 60 | for jsoned in data["snapshots"]: 61 | snapshot = VolumeSnapshot(jsoned, self) 62 | snapshots.append(snapshot) 63 | return snapshots 64 | 65 | def create_snapshot(self): 66 | self.manager.call_api("storage/%s/snapshots" % self.id, type="POST") 67 | 68 | def create_snapshot_policy(self, frequency, count): 69 | """Creates a new snapshot policy of your volume. 70 | 71 | :param frequency: (required) Snapshot frequency 72 | 73 | Validation of `frequency` is left to the packet api to avoid going out 74 | of date if any new value is introduced. 75 | The currently known values are: 76 | - 1hour, 77 | - 1day 78 | - 1week 79 | - 1month 80 | - 1year 81 | """ 82 | data = self.manager.call_api( 83 | "storage/{0}/snapshot-policies?snapshot_frequency={1}&snapshot_count={2}".format( 84 | self.id, frequency, count 85 | ), 86 | type="POST", 87 | ) 88 | return SnapshotPolicy(data, self) 89 | 90 | def clone(self): 91 | return Volume( 92 | self.manager.call_api("storage/%s/clone" % self.id, type="POST"), 93 | manager=self.manager, 94 | ) 95 | 96 | def restore(self, restore_point): 97 | self.manager.restore_volume(self.id, restore_point=restore_point) 98 | 99 | def __str__(self): 100 | return "%s" % self.id 101 | 102 | def __repr__(self): 103 | return "{}: {}".format(self.__class__.__name__, self.id) 104 | 105 | 106 | class VolumeSnapshot: 107 | def __init__(self, data, volume): 108 | self.volume = volume 109 | 110 | self.id = data["id"] 111 | self.status = data["status"] 112 | self.timestamp = data["timestamp"] 113 | self.created_at = data["created_at"] 114 | 115 | def delete(self): 116 | return self.volume.manager.call_api( 117 | "/storage/%s/snapshots/%s" % (self.volume.id, self.id), type="DELETE" 118 | ) 119 | 120 | def __str__(self): 121 | return "%s" % self.id 122 | 123 | def __repr__(self): 124 | return "{}: {}".format(self.__class__.__name__, self.id) 125 | 126 | 127 | class SnapshotPolicy: 128 | def __init__(self, data, policy): 129 | self.policy = policy 130 | 131 | self.id = data["id"] 132 | self.count = data["snapshot_count"] 133 | self.frequency = data["snapshot_frequency"] 134 | self.created_at = data["created_at"] 135 | self.updated_at = data["updated_at"] 136 | 137 | def delete(self): 138 | return self.policy.manager.call_api( 139 | "storage/snapshot-policies/%s" % self.id, type="DELETE" 140 | ) 141 | 142 | def update_snapshot_policy(self, frequency, count): 143 | """Updates the volume snapshot policy. 144 | 145 | :param frequency: (required) Snapshot frequency 146 | 147 | Validation of `frequency` is left to the packet api to avoid going out 148 | of date if any new value is introduced. 149 | The currently known values are: 150 | - 1hour, 151 | - 1day 152 | - 1week 153 | - 1month 154 | - 1year 155 | """ 156 | data = self.policy.manager.call_api( 157 | "storage/snapshot-policies/{0}?snapshot_frequency={1}&snapshot_count={2}".format( 158 | self.id, frequency, count 159 | ), 160 | type="PATCH", 161 | ) 162 | return SnapshotPolicy(data, self) 163 | 164 | def __str__(self): 165 | return "%s" % self.id 166 | 167 | def __repr__(self): 168 | return "{}: {}".format(self.__class__.__name__, self.id) 169 | -------------------------------------------------------------------------------- /packet/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | """library to interact with the Equinix Metal API""" 5 | 6 | __version__ = "1.44.3" 7 | __author__ = "Equinix Metal Engineers" 8 | __author_email__ = "support@equinixmetal.com" 9 | __license__ = "LGPL v3" 10 | __copyright__ = "Copyright (c) 2021, Equinix" 11 | 12 | from .Device import Device # noqa 13 | from .Email import Email # noqa 14 | from .Event import Event # noqa 15 | from .Facility import Facility # noqa 16 | from .Metro import Metro # noqa 17 | from .OperatingSystem import OperatingSystem # noqa 18 | from .Plan import Plan # noqa 19 | from .Project import Project # noqa 20 | from .SSHKey import SSHKey # noqa 21 | from .Volume import Volume # noqa 22 | from .BGPConfig import BGPConfig # noqa 23 | from .BGPSession import BGPSession # noqa 24 | from .DeviceBatch import DeviceBatch # noqa 25 | from .Manager import Manager # noqa 26 | from .Snapshot import Snapshot # noqa 27 | from .Organization import Organization # noqa 28 | from .Provider import Provider # noqa 29 | from .baseapi import Error # noqa 30 | from .baseapi import ResponseError # noqa 31 | -------------------------------------------------------------------------------- /packet/baseapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import json 5 | import logging 6 | import requests 7 | 8 | from packet import __version__ 9 | 10 | 11 | class Error(Exception): 12 | """Base exception class for this module""" 13 | 14 | def __init__(self, msg, cause=None): 15 | super(Error, self).__init__(msg) 16 | self._cause = cause 17 | 18 | @property 19 | def cause(self): 20 | """The underlying exception causing the error, if any.""" 21 | return self._cause 22 | 23 | 24 | class ResponseError(Error): 25 | def __init__(self, resp, data, exception=None): 26 | if not data: 27 | msg = "(empty response)" 28 | elif not isinstance(data, dict): 29 | msg = str(data) 30 | elif "error" in data: 31 | msg = data["error"] 32 | elif "errors" in data: 33 | msg = ", ".join(data["errors"]) 34 | super(ResponseError, self).__init__( 35 | "Error {0}: {1}".format(resp.status_code, msg), exception 36 | ) 37 | self._response = resp 38 | 39 | @property 40 | def response(self): 41 | """The Requests response which failed""" 42 | return self._response 43 | 44 | 45 | class JSONReadError(Error): 46 | pass 47 | 48 | 49 | class BaseAPI(object): 50 | """ 51 | Basic api class for 52 | """ 53 | 54 | def __init__(self, auth_token, consumer_token, user_agent=""): 55 | self.auth_token = auth_token 56 | self.consumer_token = consumer_token 57 | self.end_point = "api.packet.net" 58 | self._user_agent_prefix = user_agent 59 | self._log = logging.getLogger(__name__) 60 | 61 | @property 62 | def user_agent(self): 63 | return "{}packet-python/{} {}".format( 64 | self._user_agent_prefix, __version__, requests.utils.default_user_agent() 65 | ).strip() 66 | 67 | def call_api(self, method, type="GET", params=None): # noqa 68 | if params is None: 69 | params = {} 70 | 71 | url = "https://" + self.end_point + "/" + method 72 | 73 | headers = { 74 | "X-Auth-Token": self.auth_token, 75 | "X-Consumer-Token": self.consumer_token, 76 | "Content-Type": "application/json", 77 | "User-Agent": self.user_agent, 78 | } 79 | 80 | # remove token from log 81 | headers_str = str(headers).replace(self.auth_token.strip(), "TOKEN") 82 | self._log.debug("%s %s %s %s" % (type, url, params, headers_str)) 83 | try: 84 | if type == "GET": 85 | url = url + "%s" % self._parse_params(params) 86 | resp = requests.get(url, headers=headers) 87 | elif type == "POST": 88 | resp = requests.post( 89 | url, 90 | headers=headers, 91 | data=json.dumps( 92 | params, default=lambda o: o.__dict__, sort_keys=True, indent=4 93 | ), 94 | ) 95 | elif type == "DELETE": 96 | resp = requests.delete(url, headers=headers) 97 | elif type == "PATCH": 98 | resp = requests.patch(url, headers=headers, data=json.dumps(params)) 99 | else: 100 | raise Error( 101 | "method type not recognized as one of GET, POST, DELETE or PATCH: %s" 102 | % type 103 | ) 104 | except requests.exceptions.RequestException as e: # pragma: no cover 105 | raise Error("Communcations error: %s" % str(e), e) 106 | 107 | if not resp.content: 108 | data = None 109 | elif resp.headers.get("content-type", "").startswith("application/json"): 110 | try: 111 | data = resp.json() 112 | except ValueError as e: # pragma: no cover 113 | raise JSONReadError("Read failed: %s" % e.message, e) 114 | else: 115 | data = resp.content # pragma: no cover 116 | 117 | if not resp.ok: # pragma: no cover 118 | raise ResponseError(resp, data) 119 | 120 | try: 121 | resp.raise_for_status() 122 | except requests.HTTPError as e: # pragma: no cover 123 | raise ResponseError(resp, data, e) 124 | 125 | self.meta = None 126 | try: 127 | if data and data["meta"]: 128 | self.meta = data["meta"] 129 | except (KeyError, IndexError): 130 | pass 131 | 132 | return data 133 | 134 | def _parse_params(self, params): 135 | vals = list() 136 | for k, v in params.items(): 137 | vals.append(str("%s=%s" % (k, v))) 138 | return "?" + "&".join(vals) 139 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | [aliases] 4 | test=pytest 5 | 6 | [bdist_wheel] 7 | universal=1 8 | 9 | [pylama:pycodestyle] 10 | ignore = E501 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | import codecs 4 | import os.path 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from ez_setup import use_setuptools 10 | 11 | use_setuptools() 12 | from setuptools import setup 13 | 14 | 15 | with open("README.md") as readme, open("CHANGELOG.md") as changelog: 16 | long_description = readme.read() + "\n" + changelog.read() 17 | 18 | 19 | def read(rel_path): 20 | here = os.path.abspath(os.path.dirname(__file__)) 21 | with codecs.open(os.path.join(here, rel_path), "r") as fp: 22 | return fp.read() 23 | 24 | 25 | def get_version(rel_path): 26 | for line in read(rel_path).splitlines(): 27 | if line.startswith("__version__"): 28 | delim = '"' if '"' in line else "'" 29 | return line.split(delim)[1] 30 | else: 31 | raise RuntimeError("Unable to find version string.") 32 | 33 | 34 | setup( 35 | name="packet-python", 36 | version=get_version("packet/__init__.py"), 37 | description="Equinix Metal (Packet) API client", 38 | long_description=long_description, 39 | long_description_content_type="text/markdown", 40 | url="https://github.com/packethost/packet-python", 41 | author="Equinix Metal Developers", 42 | author_email="support@equinixmetal.com", 43 | license="LGPL v3", 44 | keywords="equinix metal packet api client infrastructure", 45 | packages=["packet"], 46 | install_requires="requests", 47 | setup_requires=["pytest-runner"], 48 | tests_require=["pytest", "requests-mock"], 49 | classifiers=[ 50 | "Development Status :: 5 - Production/Stable", 51 | "Intended Audience :: Developers", 52 | "Intended Audience :: Information Technology", 53 | "Topic :: Software Development :: Libraries", 54 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 55 | "Programming Language :: Python :: 2", 56 | "Programming Language :: Python :: 2.7", 57 | "Programming Language :: Python :: 3", 58 | "Programming Language :: Python :: 3.4", 59 | "Programming Language :: Python :: 3.5", 60 | "Programming Language :: Python :: 3.6", 61 | "Programming Language :: Python :: 3.7", 62 | "Programming Language :: Python :: 3.8", 63 | "Programming Language :: Python :: 3.9", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | let 4 | _pkgs = import {}; 5 | in 6 | { pkgs ? import (_pkgs.fetchFromGitHub { owner = "NixOS"; 7 | repo = "nixpkgs-channels"; 8 | # nixos-unstable @2019-05-28 9 | rev = "eccb90a2d997d65dc514253b441e515d8e0241c3"; 10 | sha256 = "0ffa84mp1fgmnqx2vn43q9pypm3ip9y67dkhigsj598d8k1chzzw"; 11 | }) {} 12 | }: 13 | 14 | with pkgs; 15 | 16 | mkShell { 17 | buildInputs = [ 18 | python3 19 | python3Packages.black 20 | python3Packages.flake8 21 | python3Packages.pylama 22 | python3Packages.pytest 23 | python3Packages.pytestcov 24 | python3Packages.requests 25 | python3Packages.requests-mock 26 | python3Packages.tox 27 | python3Packages.twine 28 | python3Packages.wheel 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/get__capacity.json: -------------------------------------------------------------------------------- 1 | {"capacity":{"ams1":{"baremetal_0":{"level":"limited","servers":"45+"},"baremetal_1":{"level":"critical","servers":"1+"},"baremetal_2":{"level":"normal","servers":"14+"},"baremetal_2a":{"level":"unavailable","servers":0},"baremetal_3":{"level":"normal","servers":"8+"}},"atl1":{"baremetal_1e":{"level":"unavailable","servers":0}},"dfw1":{"baremetal_1e":{"level":"normal","servers":"10+"}},"ewr1":{"baremetal_0":{"level":"limited","servers":"101+"},"baremetal_1":{"level":"critical","servers":"1+"},"baremetal_2":{"level":"critical","servers":"1+"},"baremetal_2a":{"level":"critical","servers":"1+"},"baremetal_3":{"level":"limited","servers":"9+"}},"iad1":{"baremetal_1e":{"level":"normal","servers":"10+"}},"lax1":{"baremetal_1e":{"level":"unavailable","servers":0}},"nrt1":{"baremetal_0":{"level":"normal","servers":"68+"},"baremetal_1":{"level":"normal","servers":"15+"},"baremetal_2":{"level":"unavailable","servers":0},"baremetal_2a":{"level":"limited","servers":"3+"}},"ord1":{"baremetal_1e":{"level":"normal","servers":"10+"}},"sea1":{"baremetal_1e":{"level":"normal","servers":"10+"}},"sjc1":{"baremetal_0":{"level":"critical","servers":"1+"},"baremetal_1":{"level":"critical","servers":"1+"},"baremetal_2":{"level":"critical","servers":"1+"},"baremetal_2a":{"level":"unavailable","servers":0},"baremetal_3":{"level":"normal","servers":"10+"}}}} -------------------------------------------------------------------------------- /test/fixtures/get_devices_9dec7266.json: -------------------------------------------------------------------------------- 1 | {"id":"e781ae1b","short_id":"e781ae1b","hostname":"test-01","description":null,"state":"active","tags":[],"billing_cycle":"hourly","user":"root","iqn":"iqn.2016-06.net.packet:device.e781ae1b","locked":true,"bonding_mode":5,"created_at":"2016-06-01T19:27:28Z","updated_at":"2016-06-21T20:28:27Z","staff":true,"allow_pxe":false,"operating_system":{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","clli":null,"description":null,"npanxx":null,"emergency_phone":null,"public":true,"address":null,"discounts":[],"facility_rooms":[{"href":"/facility-rooms/6e8d7c53"}],"provider":{"href":"/providers/61fae8b0"},"href":"/facilities/e1e9c52e"},"project":{"href":"/projects/438659f0"},"volumes":[{"href":"/storage/8a3369fc"},{"href":"/storage/7f89830d"}],"ip_addresses":[{"id":"47e2d396","address_family":4,"netmask":"255.255.255.254","public":true,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e781ae1b"},"network":"147.75.1.2","address":"147.75.1.3","gateway":"147.75.1.2","href":"/ips/47e2d396"},{"id":"009d5f19","address_family":6,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","public":true,"cidr":127,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e781ae1b"},"network":"2604:::::","address":"2604:::::","gateway":"2604:::::","href":"/ips/009d5f19"},{"id":"965c286c","address_family":4,"netmask":"255.255.255.254","public":false,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e781ae1b"},"network":"10.100.1.0","address":"10.100.1.1","gateway":"10.100.1.0","href":"/ips/965c286c"}],"metering_limits":[],"server":{"href":"/hardware/3feec4bd"},"plan":{"id":"e69c0169-4726-46ea-98f1-939c9e8a3607","slug":"baremetal_0","name":"Type 0","description":"Our Type 0 configuration is a general use \"cloud killer\" server, with a Intel Atom 2.4Ghz processor and 8GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel Atom C2550 @ 2.4Ghz"}],"memory":{"total":"8GB"},"drives":[{"count":1,"size":"80GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":false,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#c19eb1f6-d283-4d52-a81d-e83c503f7b27"},{"href":"#69525607-1a1e-4843-8f67-1dd83e6bd21a"}],"pricing":{"hour":0.05}},"userdata":"","href":"/devices/e781ae1b"} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_devices_e123s_ips.json: -------------------------------------------------------------------------------- 1 | { 2 | "ip_addresses": [ 3 | { 4 | "id": "99d5d741-3756-4ebe-a014-34ea7a2e2be1", 5 | "address_family": 4, 6 | "netmask": "255.255.255.254", 7 | "created_at": "2019-07-18T08:46:38Z", 8 | "details": null, 9 | "tags": [], 10 | "public": true, 11 | "cidr": 31, 12 | "management": true, 13 | "manageable": true, 14 | "enabled": true, 15 | "global_ip": null, 16 | "customdata": {}, 17 | "project": {}, 18 | "project_lite": {}, 19 | "facility": { 20 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 21 | "name": "Amsterdam, NL", 22 | "code": "ams1", 23 | "features": [ 24 | "baremetal", 25 | "storage", 26 | "global_ipv4", 27 | "backend_transfer", 28 | "layer_2" 29 | ], 30 | "address": { 31 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 32 | }, 33 | "ip_ranges": [ 34 | "2604:1380:2000::/36", 35 | "147.75.204.0/23", 36 | "147.75.100.0/22", 37 | "147.75.80.0/22", 38 | "147.75.32.0/23" 39 | ] 40 | }, 41 | "assigned_to": { 42 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 43 | }, 44 | "interface": { 45 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 46 | }, 47 | "network": "147.75.84.94", 48 | "address": "147.75.84.95", 49 | "gateway": "147.75.84.94", 50 | "href": "/ips/99d5d741-3756-4ebe-a014-34ea7a2e2be1" 51 | }, 52 | { 53 | "id": "06fd360b-8f6c-4f3c-a858-6ea8a9bc7cf8", 54 | "address_family": 6, 55 | "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 56 | "created_at": "2019-07-18T08:46:37Z", 57 | "details": null, 58 | "tags": [], 59 | "public": true, 60 | "cidr": 127, 61 | "management": true, 62 | "manageable": true, 63 | "enabled": true, 64 | "global_ip": null, 65 | "customdata": {}, 66 | "project": {}, 67 | "project_lite": {}, 68 | "facility": { 69 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 70 | "name": "Amsterdam, NL", 71 | "code": "ams1", 72 | "features": [ 73 | "baremetal", 74 | "storage", 75 | "global_ipv4", 76 | "backend_transfer", 77 | "layer_2" 78 | ], 79 | "address": { 80 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 81 | }, 82 | "ip_ranges": [ 83 | "2604:1380:2000::/36", 84 | "147.75.204.0/23", 85 | "147.75.100.0/22", 86 | "147.75.80.0/22", 87 | "147.75.32.0/23" 88 | ] 89 | }, 90 | "assigned_to": { 91 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 92 | }, 93 | "interface": { 94 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 95 | }, 96 | "network": "2604:1380:2000:ae00::", 97 | "address": "2604:1380:2000:ae00::1", 98 | "gateway": "2604:1380:2000:ae00::", 99 | "href": "/ips/06fd360b-8f6c-4f3c-a858-6ea8a9bc7cf8" 100 | }, 101 | { 102 | "id": "4edf9211-142e-476e-b926-ebd247fb7b66", 103 | "address_family": 4, 104 | "netmask": "255.255.255.254", 105 | "created_at": "2019-07-18T08:46:37Z", 106 | "details": null, 107 | "tags": [], 108 | "public": false, 109 | "cidr": 31, 110 | "management": true, 111 | "manageable": true, 112 | "enabled": true, 113 | "global_ip": null, 114 | "customdata": {}, 115 | "project": {}, 116 | "project_lite": {}, 117 | "facility": { 118 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 119 | "name": "Amsterdam, NL", 120 | "code": "ams1", 121 | "features": [ 122 | "baremetal", 123 | "storage", 124 | "global_ipv4", 125 | "backend_transfer", 126 | "layer_2" 127 | ], 128 | "address": { 129 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 130 | }, 131 | "ip_ranges": [ 132 | "2604:1380:2000::/36", 133 | "147.75.204.0/23", 134 | "147.75.100.0/22", 135 | "147.75.80.0/22", 136 | "147.75.32.0/23" 137 | ] 138 | }, 139 | "assigned_to": { 140 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 141 | }, 142 | "interface": { 143 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 144 | }, 145 | "network": "10.80.110.0", 146 | "address": "10.80.110.1", 147 | "gateway": "10.80.110.0", 148 | "href": "/ips/4edf9211-142e-476e-b926-ebd247fb7b66" 149 | } 150 | ] 151 | } -------------------------------------------------------------------------------- /test/fixtures/get_facilities.json: -------------------------------------------------------------------------------- 1 | {"facilities":[{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"address":null,"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},{"id":"2b70eb8f-fa18-47c0-aba7-222a842362fd","name":"Sunnyvale, CA","code":"sjc1","features":["baremetal"],"address":null,"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"id":"8e6470b3-b75e-47d1-bb93-45b225750975","name":"Amsterdam, NL","code":"ams1","features":["baremetal"],"address":null,"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_ips_e123s.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "99d5d741-3756-4ebe-a014-34ea7a2e2be1", 3 | "address_family": 4, 4 | "netmask": "255.255.255.254", 5 | "created_at": "2019-07-18T08:46:38Z", 6 | "details": null, 7 | "tags": [], 8 | "public": true, 9 | "cidr": 31, 10 | "management": true, 11 | "manageable": true, 12 | "enabled": true, 13 | "global_ip": null, 14 | "customdata": {}, 15 | "project": {}, 16 | "project_lite": {}, 17 | "facility": { 18 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 19 | "name": "Amsterdam, NL", 20 | "code": "ams1", 21 | "features": [ 22 | "baremetal", 23 | "storage", 24 | "global_ipv4", 25 | "backend_transfer", 26 | "layer_2" 27 | ], 28 | "address": { 29 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 30 | }, 31 | "ip_ranges": [ 32 | "2604:1380:2000::/36", 33 | "147.75.204.0/23", 34 | "147.75.100.0/22", 35 | "147.75.80.0/22", 36 | "147.75.32.0/23" 37 | ] 38 | }, 39 | "assigned_to": { 40 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 41 | }, 42 | "interface": { 43 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 44 | }, 45 | "network": "147.75.84.94", 46 | "address": "147.75.84.95", 47 | "gateway": "147.75.84.94", 48 | "href": "/ips/99d5d741-3756-4ebe-a014-34ea7a2e2be1" 49 | } -------------------------------------------------------------------------------- /test/fixtures/get_locations_metros.json: -------------------------------------------------------------------------------- 1 | {"metros":[{"id":"0446ac38-aae0-45f5-959c-54d26c4fc3c7","name":"Phoenix","code":"px","country":"US"},{"id":"833225d2-a35a-49cf-a67c-1f84d5d11654","name":"Marseille","code":"mr","country":"FR"},{"id":"96a57b6d-c62c-41b5-ab8e-f8d63a7f9887","name":"Washington DC","code":"dc","country":"US"},{"id":"a04e45e4-4356-458c-b25a-b72cc54bdad1","name":"Atlanta","code":"at","country":"US"},{"id":"932eecda-6808-44b9-a3be-3abef49796ef","name":"New York","code":"ny","country":"US"},{"id":"108b2cfb-246b-45e3-885a-bf3e82fce1a0","name":"Amsterdam","code":"am","country":"NL"},{"id":"5afb3744-f80d-4b49-bf21-50ede9252cfd","name":"Toronto","code":"tr","country":"CA"},{"id":"d2a8e94f-2b42-4440-9c8e-8ef01defcd27","name":"Seoul","code":"sl","country":"KR"},{"id":"d50fd052-34ec-4977-a173-ad6f9266995d","name":"Hong Kong","code":"hk","country":"HK"},{"id":"f6ada324-8226-4bfc-99a8-453d47caf2dc","name":"Singapore","code":"sg","country":"SG"},{"id":"5f72cbf6-96e4-44f2-ae60-213888fa2b9f","name":"Tokyo","code":"ty","country":"JP"},{"id":"b1ac82b2-616c-4405-9424-457ef6edf9ae","name":"Frankfurt","code":"fr","country":"DE"},{"id":"d2f09853-a1aa-4b29-aa9d-682462fd8d1d","name":"Sydney","code":"sy","country":"AU"},{"id":"5f1d3910-2059-4737-8e7d-d4907cfbf27f","name":"London","code":"ld","country":"GB"},{"id":"91fa81e8-db1a-4da1-b218-7beeaab6f8bc","name":"Kansas City","code":"kc","country":"US"},{"id":"f2f0f0b3-5359-4296-a402-56abad842f56","name":"Paris","code":"pa","country":"FR"},{"id":"56c55e99-8125-4f65-af21-a8682e033804","name":"Houston","code":"ho","country":"US"},{"id":"b8ffa6c9-3da2-4cf1-b48d-049002b208fe","name":"Seattle","code":"se","country":"US"},{"id":"2991b022-b8c4-497e-8db7-5a407c3a209b","name":"Silicon Valley","code":"sv","country":"US"},{"id":"bb059cc0-0b2a-4f5b-8a55-219e6b4240da","name":"Los Angeles","code":"la","country":"US"},{"id":"60666d92-e00f-43a8-a9f8-fddf665390ca","name":"Chicago","code":"ch","country":"US"},{"id":"d3d6b29f-042d-43b7-b3ce-0bf53d5754ca","name":"Dallas","code":"da","country":"US"},{"id":"543f8059-65f6-4c2c-b7ad-6dac5eb87085","name":"Pittsburgh","code":"pi","country":"US"},{"id":"583a553a-465a-49f3-a8a9-5fa6b44aa519","name":"Detroit","code":"dt","country":"US"}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_operating-systems.json: -------------------------------------------------------------------------------- 1 | {"operating_systems":[{"slug":"centos_7","name":"CentOS 7","distro":"centos","version":"7","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"coreos_stable","name":"CoreOS (stable)","distro":"coreos","version":"stable","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"coreos_beta","name":"CoreOS (beta)","distro":"coreos","version":"beta","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"coreos_alpha","name":"CoreOS (alpha)","distro":"coreos","version":"alpha","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"debian_8","name":"Debian 8","distro":"debian","version":"8","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"rancher","name":"RancherOS","distro":"rancher","version":"latest","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"ubuntu_14_04_image","name":"Ubuntu 14.04 LTS","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"ubuntu_16_04_image","name":"Ubuntu 16.04 LTS (beta)","distro":"ubuntu","version":"16.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"windows_2012_rc2","name":"Windows 2012 RC2","distro":"windows","version":"2012 RC2","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},{"slug":"vmware_esxi_6","name":"VMWare ESXi 6","distro":"vmware","version":"6.0.0","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]}]} -------------------------------------------------------------------------------- /test/fixtures/get_plans.json: -------------------------------------------------------------------------------- 1 | {"plans":[{"id":"49f1faba-e51b-4118-b7db-a88375f0abb5","slug":"bandwidth","name":"Outbound Bandwidth","description":"TBD","line":"bandwidth","specs":{},"available_in":[],"plan_versions":[{"href":"#931f7994-c259-4d50-975f-7158012fed69"}],"pricing":{"GB":0.05}},{"id":"e69c0169-4726-46ea-98f1-939c9e8a3607","slug":"baremetal_0","name":"Type 0","description":"Our Type 0 configuration is a general use \"cloud killer\" server, with a Intel Atom 2.4Ghz processor and 8GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel Atom C2550 @ 2.4Ghz"}],"memory":{"total":"8GB"},"drives":[{"count":1,"size":"80GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":false,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#c19eb1f6-d283-4d52-a81d-e83c503f7b27"},{"href":"#69525607-1a1e-4843-8f67-1dd83e6bd21a"}],"pricing":{"hour":0.05}},{"id":"6d1f1ffa-7912-4b78-b50d-88cc7c8ab40f","slug":"baremetal_1","name":"Type 1","description":"Our Type 1 configuration is a zippy general use server, with an Intel E3-1240 v3 processor and 32GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel E3-1240 v3"}],"memory":{"total":"32GB"},"drives":[{"count":2,"size":"120GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":true,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#fe2790c8-8e8f-4997-bbf5-2fe59f98e007"},{"href":"#7facb54b-1b89-4f44-a6b3-b0dab1125a2f"},{"href":"#4f5ec112-f740-461e-8937-1068ba5fe346"}],"pricing":{"hour":0.4}},{"id":"a3729923-fdc4-4e85-a972-aafbad3695db","slug":"baremetal_2","name":"Type 2 (Beta)","description":"Our Type 2 configuration is the perfect all purpose virtualization server, with dual E5-2650 v4 processors, 256 GB of DDR4 RAM, and six SSDs totaling 2.8 TB of storage.","line":"baremetal","specs":{"cpus":[{"count":2,"type":"Intel Xeon E5-2650 v4 @2.2GHz"}],"memory":{"total":"256GB"},"drives":[{"count":6,"size":"480GB","type":"SSD"}],"nics":[{"count":2,"type":"10Gbps"}],"features":{"raid":true,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"}],"plan_versions":[{"href":"#68eea7e2-9e38-4390-8013-ce8fb352dcce"}],"pricing":{"hour":1.25}},{"id":"741f3afb-bb2f-4694-93a0-fcbad7cd5e78","slug":"baremetal_3","name":"Type 3","description":"Our Type 3 configuration is a high core, high IO server, with dual Intel E5-2640 v3 processors, 128GB of DDR4 RAM and ultra fast NVME flash drives.","line":"baremetal","specs":{"cpus":[{"count":2,"type":"Intel E5-2640 v3"}],"memory":{"total":"128GB"},"drives":[{"count":2,"size":"120GB","type":"SSD"},{"count":1,"size":"1.6TB","type":"NVME"}],"nics":[{"count":2,"type":"10Gbps"}],"features":{"raid":true,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#5d7b25a6-dc53-4e2a-8f0d-d15ab45fe127"},{"href":"#32ad93cc-be69-408d-b85b-e6230c1df33e"},{"href":"#22f231bc-45be-4bb9-816d-6e68adad96a2"},{"href":"#9344bb8a-09da-43dc-884f-91c641e442d4"}],"pricing":{"hour":1.75}},{"id":"c85c8fe2-7edb-4d63-82da-17f6506663d5","slug":"global_ipv4","name":"Global IPv4","description":"TBD","line":"network","specs":{},"available_in":[],"plan_versions":[{"href":"#771ee12d-41ae-4b78-be41-e47bc8851f7f"}],"pricing":{"hour":0.03}},{"id":"141d0eff-d5fc-4d72-9466-0c623b66a82b","slug":"public_ipv4","name":"Public IPv4","description":"TBD","line":"network","specs":{},"available_in":[],"plan_versions":[{"href":"#a9d53f2b-69cd-476c-b6dd-41ab6e1ff499"}],"pricing":{"hour":0.005}},{"id":"87728148-3155-4992-a730-8d1e6aca8a32","slug":"storage_1","name":"Standard","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#63f8bcdb-06ce-4c27-b1bb-22de20ae8ba6"}],"pricing":{"hour":0.000104}},{"id":"d6570cfb-38fa-4467-92b3-e45d059bb249","slug":"storage_2","name":"Performance","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#9969070d-dacb-45f3-bf38-f4f8332b678e"}],"pricing":{"hour":0.000223}},{"id":"1f302828-c5aa-42e7-b219-d9c40b4e3483","slug":"storage_snapshot","name":"Snapshot","description":"TBD","line":"storage_snapshot","specs":{},"available_in":[],"plan_versions":[{"href":"#be177745-d210-4896-a5a7-942b2e30985b"}],"pricing":{"hour":0.0}}]} -------------------------------------------------------------------------------- /test/fixtures/get_projects.json: -------------------------------------------------------------------------------- 1 | {"projects":[{"id":"261a69b7","name":"test project 1","created_at":"2015-08-21T09:13:54Z","updated_at":"2016-02-28T03:45:32Z","max_devices":{"baremetal_0":10,"baremetal_1":5,"baremetal_3":1,"storage_1":1,"storage_2":1},"auto_charge":true,"billable":true,"staff":false,"members":[{"href":"/users/e34b8eec"}],"memberships":[{"href":"/memberships/89c5741a"}],"invitations":[],"payment_method":{"href":"/payment-methods/8bed021a"},"devices":[],"ssh_keys":[],"volumes":[],"price_policies":[],"discounts":[],"metering_limits":[],"href":"/projects/261a69b7"},{"id":"a4209ba5","name":"test project 2","created_at":"2015-12-12T06:18:17Z","updated_at":"2016-02-28T03:45:34Z","max_devices":{"baremetal_0":10,"baremetal_1":5,"baremetal_3":1,"storage_1":1,"storage_2":1},"auto_charge":true,"billable":true,"staff":false,"members":[{"href":"/users/7f014d49"}],"memberships":[{"href":"/memberships/d5e192c7"}],"invitations":[],"payment_method":{"href":"/payment-methods/90f69800"},"devices":[],"ssh_keys":[{"href":"/ssh-keys/aedbc017"},{"href":"/ssh-keys/e5045903"}],"volumes":[],"price_policies":[],"discounts":[],"metering_limits":[],"href":"/projects/a4209ba5"},{"id":"c9b4bd73","name":"test project 3","created_at":"2015-08-21T15:53:06Z","updated_at":"2016-02-28T03:45:35Z","max_devices":{"baremetal_0":10,"baremetal_1":5,"baremetal_3":1,"storage_1":1,"storage_2":1},"auto_charge":true,"billable":true,"staff":false,"members":[{"href":"/users/d6187c99"}],"memberships":[{"href":"/memberships/1e4f6b13"}],"invitations":[],"payment_method":{"href":"/payment-methods/0a98073b"},"devices":[],"ssh_keys":[],"volumes":[],"price_policies":[],"discounts":[],"metering_limits":[],"href":"/projects/c9b4bd73"}],"meta":{"first":{"href":"/projects?page=1"},"previous":null,"self":{"href":"/projects?page=1"},"next":{"href":"/projects?page=2"},"last":{"href":"/projects?page=187"},"total":18}} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_projects_1234_bgp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "913317a6-8baf-4142-94be-87de68474947", 3 | "status": "enabled", 4 | "deployment_type": "local", 5 | "asn": 65000, 6 | "md5": "c3RhY2twb2ludDIwMTgK", 7 | "route_object": null, 8 | "max_prefix": 10, 9 | "created_at": "2018-04-05T20:13:17Z", 10 | "requested_at": "2018-07-12T15:38:49Z", 11 | "project": { 12 | "href": "/projects/93125c2a-8b78-4d4f-a3c4-7367d6b7cca8" 13 | }, 14 | "sessions": [], 15 | "ranges": [], 16 | "href": "/bgp-configs/913317a6-8baf-4142-94be-87de68474947" 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/get_projects_438659f0.json: -------------------------------------------------------------------------------- 1 | {"id":"438659f0","name":"test project","created_at":"2016-05-22T13:23:11Z","updated_at":"2016-07-05T12:52:25Z","max_devices":{"baremetal_0":null,"baremetal_1":null,"baremetal_2":null,"baremetal_3":null,"storage_1":null,"storage_2":null},"auto_charge":true,"billable":true,"staff":false,"members":[{"href":"/users/1140617d"}],"memberships":[{"href":"/memberships/2f06a0b7"}],"invitations":[],"payment_method":{"href":"/payment-methods/b2bb4ee0"},"devices":[{"href":"/devices/e781ae1b"},{"href":"/devices/e9a2a61e"},{"href":"/devices/b5b4f31f"}],"ssh_keys":[{"href":"/ssh-keys/a3d8bebe"},{"href":"/ssh-keys/084a5dec"}],"volumes":[{"href":"/storage/f9a8a263"},{"href":"/storage/f2df1dd1"},{"href":"/storage/bf595211"},{"href":"/storage/7f89830d"},{"href":"/storage/8a3369fc"}],"price_policies":[],"discounts":[],"metering_limits":[],"href":"/projects/438659f0"} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_projects_438659f0_devices.json: -------------------------------------------------------------------------------- 1 | {"devices":[{"id":"e781ae1b","short_id":"e781ae1b","hostname":"test-01","description":null,"state":"active","tags":[],"billing_cycle":"hourly","user":"root","iqn":"iqn.2016-06.net.packet:device.e781ae1b","locked":true,"bonding_mode":5,"created_at":"2016-06-01T19:27:28Z","updated_at":"2016-06-21T20:28:27Z","staff":true,"allow_pxe":false,"operating_system":{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","clli":null,"description":null,"npanxx":null,"emergency_phone":null,"public":true,"address":null,"discounts":[],"facility_rooms":[{"href":"/facility-rooms/6e8d7c53"}],"provider":{"href":"/providers/61fae8b0"},"href":"/facilities/e1e9c52e"},"project":{"href":"/projects/438659f0"},"volumes":[{"href":"/storage/8a3369fc"},{"href":"/storage/7f89830d"}],"ip_addresses":[{"id":"47e2d396","address_family":4,"netmask":"255.255.255.254","public":true,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e781ae1b"},"network":"147.75.1.2","address":"147.75.1.3","gateway":"147.75.1.2","href":"/ips/47e2d396"},{"id":"009d5f19","address_family":6,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","public":true,"cidr":127,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e781ae1b"},"network":"2604:::::","address":"2604:::::","gateway":"2604:::::","href":"/ips/009d5f19"},{"id":"965c286c","address_family":4,"netmask":"255.255.255.254","public":false,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e781ae1b"},"network":"10.100.1.0","address":"10.100.1.1","gateway":"10.100.1.0","href":"/ips/965c286c"}],"metering_limits":[],"server":{"href":"/hardware/3feec4bd"},"plan":{"id":"e69c0169-4726-46ea-98f1-939c9e8a3607","slug":"baremetal_0","name":"Type 0","description":"Our Type 0 configuration is a general use \"cloud killer\" server, with a Intel Atom 2.4Ghz processor and 8GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel Atom C2550 @ 2.4Ghz"}],"memory":{"total":"8GB"},"drives":[{"count":1,"size":"80GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":false,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#c19eb1f6-d283-4d52-a81d-e83c503f7b27"},{"href":"#69525607-1a1e-4843-8f67-1dd83e6bd21a"}],"pricing":{"hour":0.05}},"userdata":"","href":"/devices/e781ae1b"},{"id":"e9a2a61e","short_id":"e9a2a61e","hostname":"test-02","description":null,"state":"active","tags":[],"billing_cycle":"hourly","user":"root","iqn":"iqn.2016-06.net.packet:device.e9a2a61e","locked":true,"bonding_mode":5,"created_at":"2016-06-01T18:56:44Z","updated_at":"2016-06-17T16:23:20Z","staff":true,"allow_pxe":false,"operating_system":{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","clli":null,"description":null,"npanxx":null,"emergency_phone":null,"public":true,"address":null,"discounts":[],"facility_rooms":[{"href":"/facility-rooms/6e8d7c53"}],"provider":{"href":"/providers/61fae8b0"},"href":"/facilities/e1e9c52e"},"project":{"href":"/projects/438659f0"},"volumes":[],"ip_addresses":[{"id":"20865b6b","address_family":4,"netmask":"255.255.255.254","public":true,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e9a2a61e"},"network":"147.75.1.1","address":"147.75.1.1","gateway":"147.75.1.1","href":"/ips/20865b6b"},{"id":"26961028","address_family":6,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","public":true,"cidr":127,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e9a2a61e"},"network":"2604:::::","address":"2604:::::","gateway":"2604:::::","href":"/ips/26961028"},{"id":"62d46348","address_family":4,"netmask":"255.255.255.254","public":false,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/e9a2a61e"},"network":"10.100.1.1","address":"10.100.1.1","gateway":"10.100.1.1","href":"/ips/62d46348"}],"metering_limits":[],"server":{"href":"/hardware/3c06c622"},"plan":{"id":"e69c0169-4726-46ea-98f1-939c9e8a3607","slug":"baremetal_0","name":"Type 0","description":"Our Type 0 configuration is a general use \"cloud killer\" server, with a Intel Atom 2.4Ghz processor and 8GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel Atom C2550 @ 2.4Ghz"}],"memory":{"total":"8GB"},"drives":[{"count":1,"size":"80GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":false,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#c19eb1f6-d283-4d52-a81d-e83c503f7b27"},{"href":"#69525607-1a1e-4843-8f67-1dd83e6bd21a"}],"pricing":{"hour":0.05}},"userdata":"","href":"/devices/e9a2a61e"}],"meta":{"first":{"href":"/projects/438659f0/devices?page=1"},"previous":null,"self":{"href":"/projects/438659f0/devices?page=1"},"next":null,"last":{"href":"/projects/438659f0/devices?page=1"},"total":2}} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_projects_438659f0_ips.json: -------------------------------------------------------------------------------- 1 | {"ip_addresses": [{ 2 | "id": "99d5d741-3756-4ebe-a014-34ea7a2e2be1", 3 | "address_family": 4, 4 | "netmask": "255.255.255.254", 5 | "created_at": "2019-07-18T08:46:38Z", 6 | "details": null, 7 | "tags": [], 8 | "public": true, 9 | "cidr": 31, 10 | "management": true, 11 | "manageable": true, 12 | "enabled": true, 13 | "global_ip": null, 14 | "customdata": {}, 15 | "project": {}, 16 | "project_lite": {}, 17 | "facility": { 18 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 19 | "name": "Amsterdam, NL", 20 | "code": "ams1", 21 | "features": [ 22 | "baremetal", 23 | "storage", 24 | "global_ipv4", 25 | "backend_transfer", 26 | "layer_2" 27 | ], 28 | "address": { 29 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 30 | }, 31 | "ip_ranges": [ 32 | "2604:1380:2000::/36", 33 | "147.75.204.0/23", 34 | "147.75.100.0/22", 35 | "147.75.80.0/22", 36 | "147.75.32.0/23" 37 | ] 38 | }, 39 | "assigned_to": { 40 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 41 | }, 42 | "interface": { 43 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 44 | }, 45 | "network": "147.75.84.94", 46 | "address": "147.75.84.95", 47 | "gateway": "147.75.84.94", 48 | "href": "/ips/99d5d741-3756-4ebe-a014-34ea7a2e2be1" 49 | }, 50 | { 51 | "id": "99d5d741-3756-4ebe-a014-34ea7a2e2be2", 52 | "address_family": 4, 53 | "netmask": "255.255.255.254", 54 | "created_at": "2019-07-18T08:46:38Z", 55 | "details": null, 56 | "tags": [], 57 | "public": true, 58 | "cidr": 31, 59 | "management": true, 60 | "manageable": true, 61 | "enabled": true, 62 | "global_ip": null, 63 | "customdata": {}, 64 | "project": {}, 65 | "project_lite": {}, 66 | "facility": { 67 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 68 | "name": "Amsterdam, NL", 69 | "code": "ams1", 70 | "features": [ 71 | "baremetal", 72 | "storage", 73 | "global_ipv4", 74 | "backend_transfer", 75 | "layer_2" 76 | ], 77 | "address": { 78 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 79 | }, 80 | "ip_ranges": [ 81 | "2604:1380:2000::/36", 82 | "147.75.204.0/23", 83 | "147.75.100.0/22", 84 | "147.75.80.0/22", 85 | "147.75.32.0/23" 86 | ] 87 | }, 88 | "assigned_to": { 89 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 90 | }, 91 | "interface": { 92 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 93 | }, 94 | "network": "147.75.84.94", 95 | "address": "147.75.84.95", 96 | "gateway": "147.75.84.94", 97 | "href": "/ips/99d5d741-3756-4ebe-a014-34ea7a2e2be1" 98 | }]} -------------------------------------------------------------------------------- /test/fixtures/get_projects_438659f0_storage.json: -------------------------------------------------------------------------------- 1 | {"volumes":[{"id":"f9a8a263","name":"volume-f9a8a263","description":"test volume 1","size":100,"locked":false,"billing_cycle":"hourly","state":"active","created_at":"2016-05-22T13:40:06Z","updated_at":"2016-06-17T18:36:06Z","access":{"ips":["10.144.1.1","10.144.1.2"],"iqn":"iqn.2013-05.com.daterainc:tc:01:sn:"},"project":{"href":"/projects/438659f0"},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","address":null,"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"snapshot_policies":[{"href":"/storage/snapshot-policies/73a5252a"}],"attachments":[],"plan":{"id":"87728148-3155-4992-a730-8d1e6aca8a32","slug":"storage_1","name":"Standard","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#63f8bcdb-06ce-4c27-b1bb-22de20ae8ba6"}],"pricing":{"hour":0.000104}},"href":"/storage/f9a8a263"},{"id":"f2df1dd1","name":"volume-f2df1dd1","description":"test volume 2","size":10,"locked":false,"billing_cycle":"hourly","state":"active","created_at":"2016-06-04T20:55:11Z","updated_at":"2016-06-04T20:58:16Z","access":{"ips":["10.144.1.2","10.144.1.2"],"iqn":"iqn.2013-05.com.daterainc:tc:01:sn:"},"project":{"href":"/projects/438659f0"},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"address":null,"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"snapshot_policies":[],"attachments":[{"id":"cdafcbfa","created_at":"2016-06-04T20:58:16Z","volume":{"href":"/storage/f2df1dd1"},"device":{"id":"b5b4f31f","short_id":"b5b4f31f","hostname":"test-node-01","description":null,"state":"active","tags":[],"billing_cycle":"hourly","user":"root","iqn":"iqn.2016-06.net.packet:device.b5b4f31f","locked":true,"bonding_mode":5,"created_at":"2016-06-01T19:27:28Z","updated_at":"2016-06-21T20:28:24Z","allow_pxe":false,"operating_system":{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"address":null,"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"project":{"href":"/projects/438659f0"},"volumes":[{"href":"/storage/f2df1dd1"},{"href":"/storage/bf595211"}],"ip_addresses":[{"id":"a2660aa4","address_family":4,"netmask":"255.255.255.254","public":true,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/b5b4f31f"},"network":"147.75.1.1","address":"147.75.1.1","gateway":"147.75.1.1","href":"/ips/a2660aa4"},{"id":"aa2010b3","address_family":6,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","public":true,"cidr":127,"management":true,"manageable":true,"assigned_to":{"href":"/devices/b5b4f31f"},"network":"2604:::::","address":"2604:::::","gateway":"2604:::::","href":"/ips/aa2010b3"},{"id":"d0263124","address_family":4,"netmask":"255.255.255.254","public":false,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/b5b4f31f"},"network":"10.100.1.1","address":"10.100.1.1","gateway":"10.100.1.1","href":"/ips/d0263124"}],"metering_limits":[],"server":{"href":"/hardware/6fe0f5d5"},"plan":{"id":"e69c0169-4726-46ea-98f1-939c9e8a3607","slug":"baremetal_0","name":"Type 0","description":"Our Type 0 configuration is a general use \"cloud killer\" server, with a Intel Atom 2.4Ghz processor and 8GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel Atom C2550 @ 2.4Ghz"}],"memory":{"total":"8GB"},"drives":[{"count":1,"size":"80GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":false,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#c19eb1f6-d283-4d52-a81d-e83c503f7b27"},{"href":"#69525607-1a1e-4843-8f67-1dd83e6bd21a"}],"pricing":{"hour":0.05}},"userdata":"","href":"/devices/b5b4f31f"},"href":"/storage/attachments/cdafcbfa"}],"plan":{"id":"87728148-3155-4992-a730-8d1e6aca8a32","slug":"storage_1","name":"Standard","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#63f8bcdb-06ce-4c27-b1bb-22de20ae8ba6"}],"pricing":{"hour":0.000104}},"href":"/storage/f2df1dd1"}],"meta":{"first":{"href":"/projects/438659f0/storage?page=1"},"previous":null,"self":{"href":"/projects/438659f0/storage?page=1"},"next":null,"last":{"href":"/projects/438659f0/storage?page=1"},"total":2}} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_projects_438659f1_ips.json: -------------------------------------------------------------------------------- 1 | {"ip_addresses": [{ 2 | "id": "99d5d741-3756-4ebe-a014-34ea7a2e2be1", 3 | "address_family": 4, 4 | "netmask": "255.255.255.254", 5 | "created_at": "2019-07-18T08:46:38Z", 6 | "details": null, 7 | "tags": [], 8 | "public": true, 9 | "cidr": 31, 10 | "management": true, 11 | "manageable": true, 12 | "enabled": true, 13 | "global_ip": null, 14 | "customdata": {}, 15 | "project": {}, 16 | "project_lite": {}, 17 | "facility": null, 18 | "assigned_to": { 19 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 20 | }, 21 | "interface": { 22 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 23 | }, 24 | "network": "147.75.84.94", 25 | "address": "147.75.84.95", 26 | "gateway": "147.75.84.94", 27 | "href": "/ips/99d5d741-3756-4ebe-a014-34ea7a2e2be1" 28 | }, 29 | { 30 | "id": "99d5d741-3756-4ebe-a014-34ea7a2e2be2", 31 | "address_family": 4, 32 | "netmask": "255.255.255.254", 33 | "created_at": "2019-07-18T08:46:38Z", 34 | "details": null, 35 | "tags": [], 36 | "public": true, 37 | "cidr": 31, 38 | "management": true, 39 | "manageable": true, 40 | "enabled": true, 41 | "global_ip": null, 42 | "customdata": {}, 43 | "project": {}, 44 | "project_lite": {}, 45 | "facility": { 46 | "id": "8e6470b3-b75e-47d1-bb93-45b225750975", 47 | "name": "Amsterdam, NL", 48 | "code": "ams1", 49 | "features": [ 50 | "baremetal", 51 | "storage", 52 | "global_ipv4", 53 | "backend_transfer", 54 | "layer_2" 55 | ], 56 | "address": { 57 | "href": "#0688e909-647e-4b21-bdf2-fc056d993fc5" 58 | }, 59 | "ip_ranges": [ 60 | "2604:1380:2000::/36", 61 | "147.75.204.0/23", 62 | "147.75.100.0/22", 63 | "147.75.80.0/22", 64 | "147.75.32.0/23" 65 | ] 66 | }, 67 | "assigned_to": { 68 | "href": "/devices/54ffd20d-d972-4c8d-8628-9da18e67ae17" 69 | }, 70 | "interface": { 71 | "href": "/ports/02ea0556-df04-4554-b339-760a0d227b44" 72 | }, 73 | "network": "147.75.84.94", 74 | "address": "147.75.84.95", 75 | "gateway": "147.75.84.94", 76 | "href": "/ips/99d5d741-3756-4ebe-a014-34ea7a2e2be1" 77 | }]} -------------------------------------------------------------------------------- /test/fixtures/get_ssh-keys.json: -------------------------------------------------------------------------------- 1 | {"ssh_keys":[{"id":"084a5dec-30be-415a-8937-9c615932e459","label":"tzara","key":"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAo2AptpeHO53KoVwfnOG+aHUQkXlE0Y4GUmtLdP2ofV+GnJsEnmbQNi0FRQT4qeHmdKH2caYbQh275QB8e6ROJdgEO97AaHCJ2XaGI8Eq3AqIfzuUE2lDW2k1ck/94GXVTCzDNtBf5FVVP51RxlxNgSSwkBRh2SoiezoOr/uClGoBvEM0tSDChVINkl4Sq73hVeOc2gT+ehag9kx+hv4FMvrMTJJ5WvdBwfKaWniQWcsOE61ae4vaPPMs9a8wyQ3gC9m8Y7Nsi8ylJYpC+wPzwVm/TZUaVe58r8N2kecgPMUhUD9ySwMOnIOGn4zZxcRJR4vTzbOhwQ3wImqugajpCw== welch@tzara","fingerprint":"ce:65:20:86:d2:fa:34:12:74:b9:54:b2:30:d3:51:2d","created_at":"2015-02-15T16:37:46Z","updated_at":"2015-02-15T16:37:46Z","owner":{"href":"/users/1140617d-262d-4502-a3d6-771d83c930da"},"href":"/ssh-keys/084a5dec-30be-415a-8937-9c615932e459"},{"id":"a3d8bebe-574f-427d-80ee-bc2ba17f7074","label":"canary","key":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqRMl8kmT0Xwg2ZWdNIsPtMlEMPc5TzV4eFkhRaSESkVe0KSZyrwUpaJa8/A/Bg4t3iXNd/T+JuxVJJHRupstwZSVgSERqXJi933SlLrYsOKbUpjFD0ecaL1YepHi4Bt8epZtJoLFykcDxYGTvR3wMtwkPI56ijfeaE61AAYU3teCG8xa8mPAOSD6rHfrWCcrkyu3IiY08t7mgB9Rx34/nmy4aXmu3vlsNXi16NAPbZ9cWIsgaOD+wmOwL2dpGFt/82mWuvSAEsJiyQOKyGbnUs4mXxOplYBHCpUyg2nuCO67B0H8tdv+Gjm3hBM6tXMGF3EA3gi8k3dcSiqd2BNLl root@canary.packet.net","fingerprint":"51:24:c3:3a:cd:b9:31:5d:65:09:93:b7:6e:da:3f:fa","created_at":"2016-03-24T18:57:18Z","updated_at":"2016-03-24T18:57:18Z","owner":{"href":"/users/1140617d-262d-4502-a3d6-771d83c930da"},"href":"/ssh-keys/a3d8bebe-574f-427d-80ee-bc2ba17f7074"}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_ssh-keys_084a5dec.json: -------------------------------------------------------------------------------- 1 | {"id":"084a5dec","label":"test key","key":"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAo2AptpeHO53KoVwfnOG+aHUQkXlE0Y4GUmtLdP2ofV+GnJsEnmbQNi0FRQT4qeHmdKH2caYbQh275QB8e6ROJdgEO97AaHCJ2XaGI8Eq3AqIfzuUE2lDW2k1ck/94GXVTCzDNtBf5FVVP51RxlxNgSSwkBRh2SoiezoOr/uClGoBvEM0tSDChVINkl4Sq73hVeOc2gT+ehag9kx+hv4FMvrMTJJ5WvdBwfKaWniQWcsOE61ae4vaPPMs9a8wyQ3gC9m8Y7Nsi8ylJYpC+wPzwVm/TZUaVe58r8N2kecgPMUhUD9ySwMOnIOGn4zZxcRJR4vTzbOhwQ3wImqugajpCw== welch@tzara","fingerprint":"ce:65:20:86:d2:fa:34:12:74:b9:54:b2:30:d3:51:2d","created_at":"2015-02-15T16:37:46Z","updated_at":"2015-02-15T16:37:46Z","owner":{"href":"/users/1140617d"},"href":"/ssh-keys/084a5dec"} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_storage_f9a8a263.json: -------------------------------------------------------------------------------- 1 | {"id":"f9a8a263","name":"volume-f9a8a263","description":"volume description","size":100,"locked":false,"billing_cycle":"hourly","state":"active","created_at":"2016-05-22T13:40:06Z","updated_at":"2016-06-17T18:36:06Z","access":{"ips":["10.144.1.1","10.144.1.2"],"iqn":"iqn.2013-05.com.daterainc:tc:01:sn:"},"project":{"href":"/projects/438659f0"},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","clli":null,"description":null,"npanxx":null,"emergency_phone":null,"public":true,"address":null,"discounts":[],"facility_rooms":[{"href":"/facility-rooms/6e8d7c53"}],"provider":{"href":"/providers/61fae8b0"},"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"snapshot_policies":[{"href":"/storage/snapshot-policies/73a5252a"}],"attachments":[{"href":"63f8bcdb"}],"plan":{"id":"87728148-3155-4992-a730-8d1e6aca8a32","slug":"storage_0","name":"Standard","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#63f8bcdb"}],"pricing":{"hour":0.000004}},"href":"/storage/f9a8a263"} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_storage_f9a8a263_snapshots.json: -------------------------------------------------------------------------------- 1 | {"snapshots":[{"id":"8ddc76fc","status":"available","timestamp":"1467921608.991500438","created_at":"2016-07-07T20:00:08.991Z","volume":{"href":"/storage/a2be91ef"}},{"id":"141ffa69","status":"available","timestamp":"1468411189.988721398","created_at":"2016-07-13T11:59:49.988Z","volume":{"href":"/storage/a2be91ef"}},{"id":"0533695e","status":"available","timestamp":"1468414789.989156024","created_at":"2016-07-13T12:59:49.989Z","volume":{"href":"/storage/a2be91ef"}},{"id":"4ed6005e","status":"available","timestamp":"1468418389.988653005","created_at":"2016-07-13T13:59:49.988Z","volume":{"href":"/storage/a2be91ef"}},{"id":"0d029893","status":"available","timestamp":"1468421989.985048469","created_at":"2016-07-13T14:59:49.985Z","volume":{"href":"/storage/a2be91ef"}},{"id":"83153524","status":"available","timestamp":"1468425589.98895032","created_at":"2016-07-13T15:59:49.988Z","volume":{"href":"/storage/a2be91ef"}},{"id":"a2cd093f","status":"available","timestamp":"1468429189.988680424","created_at":"2016-07-13T16:59:49.988Z","volume":{"href":"/storage/a2be91ef"}},{"id":"681b1717","status":"available","timestamp":"1468432789.985096888","created_at":"2016-07-13T17:59:49.985Z","volume":{"href":"/storage/a2be91ef"}}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/get_user.json: -------------------------------------------------------------------------------- 1 | {"id":"1140617d","short_id":"1140617d","first_name":"Aaron","last_name":"Welch","full_name":"Aaron Welch","email":"welch@packet.net","two_factor_auth":"sms","avatar_url":"https://www.gravatar.com/avatar/d248672800ce5311a76f8d6bbb3a03ed?d=https://app.packet.net/client-portal/assets/images/default_gravatar.png","max_projects":null,"credit_amount":0.0,"created_at":"2015-02-05T19:09:00Z","updated_at":"2016-07-12T15:44:04Z","timezone":"America/New_York","verified_at":"2015-02-05T19:09:00Z","verification_stage":"verified","last_login_at":"2016-07-12T18:17:13Z","features":["advanced_ips","credits_migrated","invoices_migrated","block_storage"],"title":null,"company_name":"Packet","company_url":"https://www.packet.net","fraud_details":null,"social_accounts":{},"level":{"name":"","max_projects":null,"max_services_per_project":{"baremetal_0":null,"baremetal_1":null,"baremetal_2":null,"baremetal_3":null,"storage_1":null,"storage_2":null},"max_instances_per_project":{"baremetal_0":null,"baremetal_1":null,"baremetal_2":null,"baremetal_3":null,"storage_1":null,"storage_2":null},"slug":"staff"},"emails":[{"href":"/emails/661d3b15"}],"projects":[{"href":"/projects/0491f260"},{"href":"/projects/746ea6dd"}],"payment_methods":[{"href":"/payment-methods/b2bb4ee0"}],"price_policies":[],"discounts":[],"metering_limits":[],"last_login_from":{"href":"/applications/9c7d0b35"},"company_address":null,"coupon_usages":[],"href":"/users/1140617d","phone_number":"","otp_uri":""} 2 | -------------------------------------------------------------------------------- /test/fixtures/patch_devices_e781ae1b.json: -------------------------------------------------------------------------------- 1 | {"id":"9dec7266","short_id":"9dec7266","hostname":"test123","description":null,"state":"provisioning","tags":[],"billing_cycle":"hourly","user":"root","iqn":"iqn.2016-07.net.packet:device.9dec7266","locked":false,"bonding_mode":4,"created_at":"2016-07-12T19:44:19Z","updated_at":"2016-07-12T19:47:22Z","staff":true,"provisioning_percentage":57.1428571428571,"allow_pxe":true,"operating_system":{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","clli":null,"description":null,"npanxx":null,"emergency_phone":null,"public":true,"address":null,"discounts":[],"facility_rooms":[{"href":"/facility-rooms/6e8d7c53-cef1-4c95-8ee7-eca91bf704ba"}],"provider":{"href":"/providers/61fae8b0-3ca5-4fcc-9148-4309dbf646fa"},"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"project":{"href":"/projects/438659f0-3c3b-4eb0-8737-e39f678b964a"},"volumes":[],"ip_addresses":[{"id":"2ac5c64d-6ddd-4caa-ad95-571c1eba97d5","address_family":4,"netmask":"255.255.255.254","public":true,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/9dec7266-e447-4cd9-9490-b2128ad6dc04"},"network":"147.75.196.166","address":"147.75.196.167","gateway":"147.75.196.166","href":"/ips/2ac5c64d-6ddd-4caa-ad95-571c1eba97d5"},{"id":"b340b44f-eb19-4b0d-bff5-de74f37868d4","address_family":6,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","public":true,"cidr":127,"management":true,"manageable":true,"assigned_to":{"href":"/devices/9dec7266-e447-4cd9-9490-b2128ad6dc04"},"network":"2604:1380:1:c500::4","address":"2604:1380:1:c500::5","gateway":"2604:1380:1:c500::4","href":"/ips/b340b44f-eb19-4b0d-bff5-de74f37868d4"},{"id":"7087ce4c-9fa6-48aa-bea5-1d8460e059c4","address_family":4,"netmask":"255.255.255.254","public":false,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/9dec7266-e447-4cd9-9490-b2128ad6dc04"},"network":"10.100.84.4","address":"10.100.84.5","gateway":"10.100.84.4","href":"/ips/7087ce4c-9fa6-48aa-bea5-1d8460e059c4"}],"provisioning_events":[{"id":"a9b31a00-3579-42c8-9039-5a9e36cd413f","type":"provisioning.101","body":"Provisioning started","created_at":"2016-07-12T19:44:19Z","context":null,"whodunnit":null,"relationships":[{"href":"#74550b01-6839-4974-b0a7-672320334535"}],"interpolated":"Provisioning started","href":"/events/a9b31a00-3579-42c8-9039-5a9e36cd413f"},{"id":"4dc7090c-83a5-43af-abb8-596c53d759f9","type":"provisioning.102","body":"Network configured with addresses 147.75.196.167, 2604:1380:1:c500::5, and 10.100.84.5","created_at":"2016-07-12T19:45:45Z","context":null,"whodunnit":null,"relationships":[{"href":"#9e978000-45d1-4262-92f0-054c803b778c"}],"interpolated":"Network configured with addresses 147.75.196.167, 2604:1380:1:c500::5, and 10.100.84.5","href":"/events/4dc7090c-83a5-43af-abb8-596c53d759f9"},{"id":"ab7c6b7d-56aa-4cc7-8e3c-d6b917d16765","type":"provisioning.103","body":"Configuration written, restarting device","created_at":"2016-07-12T19:45:56Z","context":null,"whodunnit":null,"relationships":[{"href":"#c4913f52-1d61-47ba-a8d8-e68e0f6909ba"}],"interpolated":"Configuration written, restarting device","href":"/events/ab7c6b7d-56aa-4cc7-8e3c-d6b917d16765"},{"id":"e8974940-d32d-4b02-b7d6-84b425a97f3e","type":"provisioning.104","body":"Connected to magic install system","created_at":"2016-07-12T19:47:22Z","context":{"ip":"198.41.235.149","app_id":"84913430-7aa2-4c9f-91df-8d38c98beb2a","request_id":"f33a20f8-ca42-43cf-88e5-310ec6efe138","user_agent":"Go-http-client/1.1"},"whodunnit":{"href":"/users/0ff7f54a-8999-4102-8c74-f64813ebc57c"},"relationships":[{"href":"#69642f8e-5f12-4add-be0f-db563447db83"}],"interpolated":"Connected to magic install system","href":"/events/e8974940-d32d-4b02-b7d6-84b425a97f3e"},{"id":null,"type":"provisioning.107","body":"Server networking interfaces configured","created_at":null,"context":null,"whodunnit":null,"relationships":[],"interpolated":"Server networking interfaces configured"},{"id":null,"type":"provisioning.109","body":"Installation finished, rebooting server","created_at":null,"context":null,"whodunnit":null,"relationships":[],"interpolated":"Installation finished, rebooting server"},{"id":null,"type":"provisioning.109","body":"Installation finished, rebooting server","created_at":null,"context":null,"whodunnit":null,"relationships":[],"interpolated":"Installation finished, rebooting server"}],"metering_limits":[],"server":{"href":"/hardware/2fa510fb-c520-4c9c-ab29-8d9c760d78d6"},"plan":{"id":"6d1f1ffa-7912-4b78-b50d-88cc7c8ab40f","slug":"baremetal_1","name":"Type 1","description":"Our Type 1 configuration is a zippy general use server, with an Intel E3-1240 v3 processor and 32GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel E3-1240 v3"}],"memory":{"total":"32GB"},"drives":[{"count":2,"size":"120GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":true,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f-fa18-47c0-aba7-222a842362fd"},{"href":"/facilities/8e6470b3-b75e-47d1-bb93-45b225750975"},{"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"}],"plan_versions":[{"href":"#fe2790c8-8e8f-4997-bbf5-2fe59f98e007"},{"href":"#7facb54b-1b89-4f44-a6b3-b0dab1125a2f"},{"href":"#4f5ec112-f740-461e-8937-1068ba5fe346"}],"pricing":{"hour":0.4}},"userdata":"","root_password":"2fjyehdks3","href":"/devices/9dec7266-e447-4cd9-9490-b2128ad6dc04","crypted_root_password":"$6$z3ldgx5x3co71IDk$ksrNsz0zZwOPL8WdYrWMAU8R0LYTGc84Q6Vv2iJLdQb9KGLGNO6w820tgef2ZhEmgjN5qXDug/1HUvg2xJsNa/"} 2 | -------------------------------------------------------------------------------- /test/fixtures/patch_projects_438659f0.json: -------------------------------------------------------------------------------- 1 | {"id":"438659f0","name":"edited name","created_at":"2016-05-22T13:23:11Z","updated_at":"2016-07-05T12:52:25Z","max_devices":{"baremetal_0":null,"baremetal_1":null,"baremetal_2":null,"baremetal_3":null,"storage_1":null,"storage_2":null},"auto_charge":true,"billable":true,"staff":false,"members":[{"href":"/users/1140617d"}],"memberships":[{"href":"/memberships/2f06a0b7"}],"invitations":[],"payment_method":{"href":"/payment-methods/b2bb4ee0"},"devices":[{"href":"/devices/e781ae1b"},{"href":"/devices/e9a2a61e"},{"href":"/devices/b5b4f31f"}],"ssh_keys":[{"href":"/ssh-keys/a3d8bebe"},{"href":"/ssh-keys/084a5dec"}],"volumes":[{"href":"/storage/f9a8a263"},{"href":"/storage/f2df1dd1"},{"href":"/storage/bf595211"},{"href":"/storage/7f89830d"},{"href":"/storage/8a3369fc"}],"price_policies":[],"discounts":[],"metering_limits":[],"href":"/projects/438659f0"} 2 | -------------------------------------------------------------------------------- /test/fixtures/patch_ssh-keys_084a5dec.json: -------------------------------------------------------------------------------- 1 | {"id":"084a5dec","label":"updated label","key":"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAo2AptpeHO53KoVwfnOG+aHUQkXlE0Y4GUmtLdP2ofV+GnJsEnmbQNi0FRQT4qeHmdKH2caYbQh275QB8e6ROJdgEO97AaHCJ2XaGI8Eq3AqIfzuUE2lDW2k1ck/94GXVTCzDNtBf5FVVP51RxlxNgSSwkBRh2SoiezoOr/uClGoBvEM0tSDChVINkl4Sq73hVeOc2gT+ehag9kx+hv4FMvrMTJJ5WvdBwfKaWniQWcsOE61ae4vaPPMs9a8wyQ3gC9m8Y7Nsi8ylJYpC+wPzwVm/TZUaVe58r8N2kecgPMUhUD9ySwMOnIOGn4zZxcRJR4vTzbOhwQ3wImqugajpCw==","fingerprint":"ce:65:20:86:d2:fa:34:12:74:b9:54:b2:30:d3:51:2d","created_at":"2015-02-15T16:37:46Z","updated_at":"2015-02-15T16:37:46Z","owner":{"href":"/users/1140617d"},"href":"/ssh-keys/084a5dec"} 2 | -------------------------------------------------------------------------------- /test/fixtures/patch_storage_f9a8a263.json: -------------------------------------------------------------------------------- 1 | {"id":"f9a8a263","name":"volume-f9a8a263","description":"updated description","size":100,"locked":false,"billing_cycle":"hourly","state":"active","created_at":"2016-05-22T13:40:06Z","updated_at":"2016-06-17T18:36:06Z","access":{"ips":["10.144.1.1","10.144.1.2"],"iqn":"iqn.2013-05.com.daterainc:tc:01:sn:"},"project":{"href":"/projects/438659f0"},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"address":null,"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"snapshot_policies":[{"href":"/storage/snapshot-policies/73a5252a"}],"attachments":[],"plan":{"id":"87728148-3155-4992-a730-8d1e6aca8a32","slug":"storage_0","name":"Standard","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#63f8bcdb"}],"pricing":{"hour":0.000004}},"href":"/storage/f9a8a263"} 2 | -------------------------------------------------------------------------------- /test/fixtures/post__capacity.json: -------------------------------------------------------------------------------- 1 | {"servers":[{"facility":"ewr1","plan":"baremetal_0","quantity":10,"available":true}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/post__capacity_metros.json: -------------------------------------------------------------------------------- 1 | {"servers":[{"metro":"sv","plan":"baremetal_0","quantity":10,"available":true}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/post_devices_e781ae1b_actions.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/fixtures/post_projects.json: -------------------------------------------------------------------------------- 1 | {"id":"438659f0","name":"test project","created_at":"2016-05-22T13:23:11Z","updated_at":"2016-07-05T12:52:25Z","max_devices":{"baremetal_0":null,"baremetal_1":null,"baremetal_2":null,"baremetal_3":null,"storage_1":null,"storage_2":null},"auto_charge":true,"billable":true,"staff":true,"members":[{"href":"/users/1140617d"}],"memberships":[{"href":"/memberships/2f06a0b7"}],"invitations":[],"payment_method":{"href":"/payment-methods/b2bb4ee0"},"devices":[],"ssh_keys":[{"href":"/ssh-keys/a3d8bebe"},{"href":"/ssh-keys/084a5dec"}],"volumes":[],"price_policies":[],"discounts":[],"metering_limits":[],"href":"/projects/438659f0"} 2 | -------------------------------------------------------------------------------- /test/fixtures/post_projects_438659f0_devices.json: -------------------------------------------------------------------------------- 1 | {"id":"9dec7266","short_id":"9dec7266","hostname":"test123","description":null,"state":"provisioning","tags":[],"billing_cycle":"hourly","user":"root","iqn":"iqn.2016-07.net.packet:device.9dec7266","locked":false,"bonding_mode":4,"created_at":"2016-07-12T19:44:19Z","updated_at":"2016-07-12T19:47:22Z","staff":true,"provisioning_percentage":57.1428571428571,"allow_pxe":true,"operating_system":{"slug":"ubuntu_14_04","name":"Ubuntu 14.04 LTS (legacy)","distro":"ubuntu","version":"14.04","provisionable_on":["baremetal_0","baremetal_1","baremetal_2","baremetal_3"]},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"internal_name":"Parsippany, NJ","clli":null,"description":null,"npanxx":null,"emergency_phone":null,"public":true,"address":null,"discounts":[],"facility_rooms":[{"href":"/facility-rooms/6e8d7c53"}],"provider":{"href":"/providers/61fae8b0"},"href":"/facilities/e1e9c52e"},"project":{"href":"/projects/438659f0"},"volumes":[],"ip_addresses":[{"id":"2ac5c64d","address_family":4,"netmask":"255.255.255.254","public":true,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/9dec7266"},"network":"147.75.1.1","address":"147.75.1.1","gateway":"147.75.1.1","href":"/ips/2ac5c64d"},{"id":"b340b44f","address_family":6,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","public":true,"cidr":127,"management":true,"manageable":true,"assigned_to":{"href":"/devices/9dec7266"},"network":"2604:::::","address":"2604:::::","gateway":"2604:::::","href":"/ips/b340b44f"},{"id":"7087ce4c","address_family":4,"netmask":"255.255.255.254","public":false,"cidr":31,"management":true,"manageable":true,"assigned_to":{"href":"/devices/9dec7266"},"network":"10.100.0.1","address":"10.100.0.1","gateway":"10.100.0.1","href":"/ips/7087ce4c"}],"provisioning_events":[{"id":"a9b31a00","type":"provisioning.101","body":"Provisioning started","created_at":"2016-07-12T19:44:19Z","context":null,"whodunnit":null,"relationships":[{"href":"#74550b01"}],"interpolated":"Provisioning started","href":"/events/a9b31a00"},{"id":"4dc7090c","type":"provisioning.102","body":"Network configured with addresses 147.75.1.1, 2604:::::, and 10.100.1.1","created_at":"2016-07-12T19:45:45Z","context":null,"whodunnit":null,"relationships":[{"href":"#9e978000"}],"interpolated":"Network configured with addresses 147.75.1.1, 2604:::::, and 10.100.1.1","href":"/events/4dc7090c"},{"id":"ab7c6b7d","type":"provisioning.103","body":"Configuration written, restarting device","created_at":"2016-07-12T19:45:56Z","context":null,"whodunnit":null,"relationships":[{"href":"#c4913f52"}],"interpolated":"Configuration written, restarting device","href":"/events/ab7c6b7d"},{"id":"e8974940","type":"provisioning.104","body":"Connected to magic install system","created_at":"2016-07-12T19:47:22Z","context":{"ip":"198.41.1.1","app_id":"84913430","request_id":"f33a20f8","user_agent":"Go-http-client/1.1"},"whodunnit":{"href":"/users/0ff7f54a"},"relationships":[{"href":"#69642f8e"}],"interpolated":"Connected to magic install system","href":"/events/e8974940"},{"id":null,"type":"provisioning.107","body":"Server networking interfaces configured","created_at":null,"context":null,"whodunnit":null,"relationships":[],"interpolated":"Server networking interfaces configured"},{"id":null,"type":"provisioning.109","body":"Installation finished, rebooting server","created_at":null,"context":null,"whodunnit":null,"relationships":[],"interpolated":"Installation finished, rebooting server"},{"id":null,"type":"provisioning.109","body":"Installation finished, rebooting server","created_at":null,"context":null,"whodunnit":null,"relationships":[],"interpolated":"Installation finished, rebooting server"}],"metering_limits":[],"server":{"href":"/hardware/2fa510fb"},"plan":{"id":"6d1f1ffa","slug":"baremetal_1","name":"Type 1","description":"Our Type 1 configuration is a zippy general use server, with an Intel E3-1240 v3 processor and 32GB of RAM.","line":"baremetal","specs":{"cpus":[{"count":1,"type":"Intel E3-1240 v3"}],"memory":{"total":"32GB"},"drives":[{"count":2,"size":"120GB","type":"SSD"}],"nics":[{"count":2,"type":"1Gbps"}],"features":{"raid":true,"txt":true}},"available_in":[{"href":"/facilities/2b70eb8f"},{"href":"/facilities/8e6470b3"},{"href":"/facilities/e1e9c52e"}],"plan_versions":[{"href":"#fe2790c8"},{"href":"#7facb54b"},{"href":"#4f5ec112"}],"pricing":{"hour":0.4}},"userdata":"","root_password":"","href":"/devices/9dec7266","crypted_root_password":""} 2 | -------------------------------------------------------------------------------- /test/fixtures/post_projects_438659f0_storage.json: -------------------------------------------------------------------------------- 1 | {"id":"f9a8a263","name":"volume-f9a8a263","description":"volume description","size":100,"locked":false,"billing_cycle":"hourly","state":"active","created_at":"2016-05-22T13:40:06Z","updated_at":"2016-06-17T18:36:06Z","access":{"ips":["10.144.1.1","10.144.1.2"],"iqn":"iqn.2013-05.com.daterainc:tc:01:sn:"},"project":{"href":"/projects/438659f0"},"facility":{"id":"e1e9c52e-a0bc-4117-b996-0fc94843ea09","name":"Parsippany, NJ","code":"ewr1","features":["baremetal","storage"],"address":null,"href":"/facilities/e1e9c52e-a0bc-4117-b996-0fc94843ea09"},"snapshot_policies":[{"href":"/storage/snapshot-policies/73a5252a"}],"attachments":[],"plan":{"id":"87728148-3155-4992-a730-8d1e6aca8a32","slug":"storage_0","name":"Standard","description":"TBD","line":"storage","specs":{},"available_in":[],"plan_versions":[{"href":"#63f8bcdb"}],"pricing":{"hour":0.000004}},"href":"/storage/f9a8a263"} 2 | -------------------------------------------------------------------------------- /test/fixtures/post_ssh-keys.json: -------------------------------------------------------------------------------- 1 | {"id":"084a5dec","label":"sshkey-name","key":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI4pIqzpb5g3992h+yr527VRcaB68KE4vPjWPPoiQws49KIs2NMcOzS9QE4641uW1u5ML2HgQdfYKMF/YFGnI1Y6xV637DjhDyZYV9LasUH49npSSJjsBcsk9JGfUpNAOdcgpFzK8V90eiOrOC5YncxdwwG8pwjFI9nNVPCl4hYEu1iXdyysHvkFfS2fklsNjLWrzfafPlaen+qcBxygCA0sFdW/7er50aJeghdBHnE2WhIKLUkJxnKadznfAge7oEe+3LLAPfP+3yHyvp2+H0IzmVfYvAjnzliYetqQ8pg5ZW2BiJzvqz5PebGS70y/ySCNW1qQmJURK/Wc1bt9en","fingerprint":"ce:65:20:86:d2:fa:34:12:74:b9:54:b2:30:d3:51:2d","created_at":"2015-02-15T16:37:46Z","updated_at":"2015-02-15T16:37:46Z","owner":{"href":"/users/1140617d"},"href":"/ssh-keys/084a5dec"} 2 | -------------------------------------------------------------------------------- /test/fixtures/post_storage_f9a8a263_attachments.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/fixtures/post_storage_f9a8a263_clone.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/fixtures/post_storage_f9a8a263_snapshots.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/test_baseapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import sys 5 | import unittest 6 | 7 | import packet 8 | 9 | 10 | class obj(object): 11 | def __init__(self, dict_): 12 | self.__dict__.update(dict_) 13 | 14 | 15 | class ErrorTest(unittest.TestCase): 16 | def test_cause(self): 17 | msg = "boom" 18 | cause = "cause" 19 | error = packet.Error(msg, cause) 20 | self.assertIn(error.cause, cause) 21 | 22 | 23 | class BaseAPITest(unittest.TestCase): 24 | def setUp(self): 25 | self.auth_token = "fake_auth_token" 26 | self.consumer_token = "fake_consumer_token" 27 | self.end_point = "api.packet.net" 28 | self._user_agent_prefix = "fake_user_agent" 29 | 30 | def test_init_all(self): 31 | base = packet.baseapi.BaseAPI( 32 | self.auth_token, self.consumer_token, self._user_agent_prefix 33 | ) 34 | self.assertEqual(base.end_point, self.end_point) 35 | self.assertEqual(base.auth_token, self.auth_token) 36 | self.assertEqual(base.consumer_token, self.consumer_token) 37 | self.assertEqual(base._user_agent_prefix, self._user_agent_prefix) 38 | 39 | def test_call_api_with_end_point(self): 40 | base = packet.baseapi.BaseAPI( 41 | self.auth_token, self.consumer_token, self._user_agent_prefix 42 | ) 43 | 44 | if int(sys.version[0]) == 3: 45 | self.assertRaisesRegex( 46 | packet.Error, 47 | "method type not recognized as one of", 48 | base.call_api, 49 | "fake_path", 50 | "bad_method", 51 | ) 52 | 53 | 54 | class ResponseErrorTest(unittest.TestCase): 55 | def setUp(self): 56 | self.resp500 = obj({"status_code": 500}) 57 | self.errBoom = {"error": "boom"} 58 | self.errBangBoom = {"errors": ["bang", "boom"]} 59 | self.exception = Exception("x") 60 | 61 | def test_init_empty(self): 62 | error = packet.ResponseError(self.resp500, None, None) 63 | self.assertIn("empty", str(error)) 64 | 65 | def test_init_string(self): 66 | error = packet.ResponseError(self.resp500, "whoops", None) 67 | self.assertIn("whoops", str(error)) 68 | 69 | def test_init_error(self): 70 | error = packet.ResponseError(self.resp500, self.errBoom, self.exception) 71 | self.assertIn("Error 500: boom", str(error)) 72 | self.assertEqual(500, error.response.status_code) 73 | self.assertEqual(self.exception, error.cause) 74 | 75 | def test_init_errors(self): 76 | error = packet.ResponseError(self.resp500, self.errBangBoom, self.exception) 77 | self.assertIn("Error 500: bang, boom", str(error)) 78 | self.assertEqual(500, error.response.status_code) 79 | self.assertEqual(self.exception, error.cause) 80 | 81 | 82 | if __name__ == "__main__": 83 | sys.exit(unittest.main()) 84 | -------------------------------------------------------------------------------- /test/test_batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import time 7 | import unittest 8 | import packet 9 | 10 | from datetime import datetime 11 | 12 | 13 | @unittest.skipIf( 14 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 15 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 16 | ) 17 | class TestBatches(unittest.TestCase): 18 | @classmethod 19 | def setUpClass(self): 20 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 21 | org_id = self.manager.list_organizations()[0].id 22 | self.project = self.manager.create_organization_project( 23 | org_id=org_id, 24 | name="Int-Tests-Batch_{}".format( 25 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 26 | ), 27 | ) 28 | self.batches = list() 29 | 30 | def test_create_batch(self): 31 | params = list() 32 | batch01 = packet.DeviceBatch( 33 | { 34 | "hostname": "batchtest01", 35 | "quantity": 1, 36 | "facility": "ewr1", 37 | "operating_system": "centos_7", 38 | "plan": "baremetal_0", 39 | } 40 | ) 41 | 42 | params.append(batch01) 43 | data = self.manager.create_batch(project_id=self.project.id, params=params) 44 | self.batches = data 45 | time.sleep(10) 46 | 47 | def test_list_batches(self): 48 | self.manager.list_batches(project_id=self.project.id) 49 | 50 | def test_delete_batches(self): 51 | self.batches = self.manager.list_batches(project_id=self.project.id) 52 | for batch in self.batches: 53 | self.manager.delete_batch(batch.id, remove_associated_instances=True) 54 | 55 | @classmethod 56 | def tearDownClass(self): 57 | devices = self.manager.list_devices(project_id=self.project.id) 58 | for device in devices: 59 | if device.hostname == "batchtest01": 60 | if device.state != "active": 61 | while True: 62 | if self.manager.get_device(device.id).state != "active": 63 | time.sleep(2) 64 | else: 65 | device.delete() 66 | break 67 | self.project.delete() 68 | 69 | 70 | if __name__ == "__main__": 71 | sys.exit(unittest.main()) 72 | -------------------------------------------------------------------------------- /test/test_device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import time 8 | import packet 9 | 10 | from datetime import datetime 11 | 12 | 13 | @unittest.skipIf( 14 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 15 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 16 | ) 17 | class TestDevice(unittest.TestCase): 18 | @classmethod 19 | def setUpClass(self): 20 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 21 | org_id = self.manager.list_organizations()[0].id 22 | self.project = self.manager.create_organization_project( 23 | org_id=org_id, 24 | name="Int-Tests-Device_{}".format( 25 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 26 | ), 27 | ) 28 | 29 | self.manager.enable_project_bgp_config( 30 | project_id=self.project.id, deployment_type="local", asn=65000 31 | ) 32 | 33 | self.device = self.manager.create_device( 34 | self.project.id, "devicetest", "baremetal_0", "ewr1", "centos_7" 35 | ) 36 | 37 | while True: 38 | if self.manager.get_device(self.device.id).state == "active": 39 | break 40 | time.sleep(2) 41 | 42 | def test_get_device(self): 43 | device = self.manager.get_device(self.device.id) 44 | self.assertEqual(device.hostname, self.device.hostname) 45 | 46 | def test_list_devices(self): 47 | devices = self.manager.list_devices(self.project.id) 48 | for device in devices: 49 | if device.id is self.device.id: 50 | break 51 | self.assertRaises(TypeError) 52 | 53 | def test_update_device(self): 54 | self.device.hostname = "newname" 55 | self.device.update() 56 | device = self.manager.get_device(self.device.id) 57 | self.assertEqual(self.device.hostname, device.hostname) 58 | 59 | def test_create_bgp_session(self): 60 | bgp_session = self.manager.create_bgp_session( 61 | self.device.id, address_family="ipv4" 62 | ) 63 | self.assertIsNotNone(bgp_session) 64 | 65 | def test_get_bgp_sessions(self): 66 | data = self.manager.get_bgp_sessions(self.device.id) 67 | self.assertIsNotNone(self, data) 68 | 69 | def test_get_device_events(self): 70 | events = self.manager.list_device_events(self.device.id) 71 | self.assertGreater(len(events), 0) 72 | 73 | def test_get_device_ips(self): 74 | ips = self.device.ips() 75 | self.assertTrue(len(ips) > 0) 76 | 77 | @classmethod 78 | def tearDownClass(self): 79 | self.device.delete() 80 | self.project.delete() 81 | 82 | 83 | if __name__ == "__main__": 84 | sys.exit(unittest.main()) 85 | -------------------------------------------------------------------------------- /test/test_email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import packet 8 | import random 9 | 10 | 11 | @unittest.skipIf( 12 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 13 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 14 | ) 15 | class TestEmail(unittest.TestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 19 | 20 | cls.email = cls.manager.add_email( 21 | "john.doe{}@packet.com".format(random.randint(1, 1001)) 22 | ) 23 | 24 | def test_get_email(self): 25 | email = self.manager.get_email(self.email.id) 26 | self.assertEqual(email.address, self.email.address) 27 | 28 | def test_update_email(self): 29 | self.email.address = "john.doe{}@packet.com".format(random.randint(1, 1001)) 30 | self.email.update() 31 | # email address cannot be updated? 32 | # email = self.manager.get_email(self.email.id) 33 | # self.assertEqual(email.address, self.email.address) 34 | 35 | @classmethod 36 | def tearDownClass(cls): 37 | cls.email.delete() 38 | 39 | 40 | if __name__ == "__main__": 41 | sys.exit(unittest.main()) 42 | -------------------------------------------------------------------------------- /test/test_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import packet 8 | 9 | from datetime import datetime 10 | 11 | 12 | @unittest.skipIf( 13 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 14 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 15 | ) 16 | class TestEvent(unittest.TestCase): 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 20 | 21 | cls.events = cls.manager.list_events() 22 | 23 | org_id = cls.manager.list_organizations()[0].id 24 | cls.project = cls.manager.create_organization_project( 25 | org_id=org_id, 26 | name="Int-Tests-Events_{}".format( 27 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 28 | ), 29 | ) 30 | 31 | def test_list_events(self): 32 | self.assertTrue(len(self.events) > 0) 33 | 34 | def test_get_event(self): 35 | event = self.manager.get_event(self.events[0].id) 36 | self.assertEqual(event.id, self.events[0].id) 37 | 38 | def test_get_project_events(self): 39 | events = self.manager.list_project_events(self.project.id) 40 | self.assertGreaterEqual(len(events), 0) 41 | 42 | @classmethod 43 | def tearDownClass(cls): 44 | cls.project.delete() 45 | 46 | 47 | if __name__ == "__main__": 48 | sys.exit(unittest.main()) 49 | -------------------------------------------------------------------------------- /test/test_ips.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import time 7 | import unittest 8 | import packet 9 | 10 | from datetime import datetime 11 | 12 | 13 | @unittest.skipIf( 14 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 15 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 16 | ) 17 | class TestIps(unittest.TestCase): 18 | @classmethod 19 | def setUpClass(cls): 20 | cls.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 21 | 22 | org_id = cls.manager.list_organizations()[0].id 23 | cls.project = cls.manager.create_organization_project( 24 | org_id=org_id, 25 | name="Int-Tests-IPs_{}".format( 26 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 27 | ), 28 | ) 29 | 30 | cls.ip_block = cls.manager.reserve_ip_address( 31 | project_id=cls.project.id, 32 | type="public_ipv4", 33 | quantity=1, 34 | facility="ewr1", 35 | details="delete me", 36 | tags=["deleteme"], 37 | ) 38 | 39 | cls.device = cls.manager.create_device( 40 | cls.project.id, "iptest", "baremetal_0", "ewr1", "centos_7" 41 | ) 42 | 43 | while True: 44 | if cls.manager.get_device(cls.device.id).state == "active": 45 | break 46 | time.sleep(2) 47 | 48 | def test_reserve_ip_address(self): 49 | self.assertEqual(32, self.ip_block.cidr) 50 | self.assertEqual("delete me", self.ip_block.details) 51 | 52 | def test_list_project_ips(self): 53 | ips = self.manager.list_project_ips(self.project.id) 54 | self.assertGreater(len(ips), 0) 55 | 56 | def test_create_device_ip(self): 57 | ip = self.manager.create_device_ip( 58 | self.device.id, address=self.ip_block.address 59 | ) 60 | self.assertIsNotNone(ip) 61 | self.assertEqual(ip.address, self.ip_block.address) 62 | 63 | @classmethod 64 | def tearDownClass(cls): 65 | cls.device.delete() 66 | cls.ip_block.delete() 67 | cls.project.delete() 68 | 69 | 70 | if __name__ == "__main__": 71 | sys.exit(unittest.main()) 72 | -------------------------------------------------------------------------------- /test/test_organization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import packet 8 | 9 | 10 | @unittest.skipIf( 11 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 12 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 13 | ) 14 | class TestOrganization(unittest.TestCase): 15 | @classmethod 16 | def setUpClass(self): 17 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 18 | orgs = self.manager.list_organizations() 19 | self.org_id = orgs[0].id 20 | 21 | def test_organization(self): 22 | org = self.manager.get_organization(org_id=self.org_id) 23 | self.assertEqual(self.org_id, org.id) 24 | 25 | def test_create_organization_project(self): 26 | project = self.manager.create_organization_project( 27 | org_id=self.org_id, 28 | name="live-tests-project", 29 | payment_method_id=None, 30 | customdata={"tag": "delete me"}, 31 | ) 32 | self.assertIsNotNone(project) 33 | project.delete() 34 | 35 | def test_list_organization_projects(self): 36 | projects = self.manager.list_organization_projects(org_id=self.org_id) 37 | self.assertGreater(len(projects), 0) 38 | 39 | def test_list_organization_devices(self): 40 | devices = self.manager.list_organization_devices(org_id=self.org_id) 41 | self.assertGreaterEqual(len(devices), 0) 42 | 43 | @classmethod 44 | def tearDownClass(self): 45 | pass 46 | 47 | 48 | if __name__ == "__main__": 49 | sys.exit(unittest.main()) 50 | -------------------------------------------------------------------------------- /test/test_packet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import sys 5 | import json 6 | import unittest 7 | 8 | import packet 9 | import requests_mock 10 | 11 | 12 | class PacketManagerTest(unittest.TestCase): 13 | def setUp(self): 14 | self.manager = PacketMockManager(auth_token="foo") 15 | 16 | def test_get_user(self): 17 | user = self.manager.get_user() 18 | 19 | self.assertEqual(user.get("full_name"), "Aaron Welch") 20 | 21 | def test_list_facilities(self): 22 | facilities = self.manager.list_facilities() 23 | for facility in facilities: 24 | str(facility) 25 | repr(facility) 26 | self.assertIsInstance(facility, packet.Facility) 27 | 28 | def test_list_metros(self): 29 | metros = self.manager.list_metros() 30 | for metro in metros: 31 | str(metro) 32 | repr(metro) 33 | self.assertIsInstance(metro, packet.Metro) 34 | 35 | def test_list_plans(self): 36 | plans = self.manager.list_plans() 37 | for plan in plans: 38 | str(plan) 39 | repr(plan) 40 | self.assertIsInstance(plan, packet.Plan) 41 | 42 | def test_list_operating_systems(self): 43 | oss = self.manager.list_operating_systems() 44 | for o in oss: 45 | str(o) 46 | repr(o) 47 | self.assertIsInstance(o, packet.OperatingSystem) 48 | 49 | def test_list_projects(self): 50 | projects = self.manager.list_projects() 51 | self.assertIsInstance(projects, list) 52 | for project in projects: 53 | str(project) 54 | repr(project) 55 | self.assertIsInstance(project, packet.Project) 56 | 57 | def test_get_project(self): 58 | project = self.manager.get_project("438659f0") 59 | self.assertIsInstance(project, packet.Project) 60 | 61 | def test_create_project(self): 62 | project = self.manager.create_project("test project") 63 | self.assertIsInstance(project, packet.Project) 64 | 65 | def test_update_project(self): 66 | name = "updated name" 67 | project = self.manager.get_project("438659f0") 68 | project.name = name 69 | project.update() 70 | self.assertEqual(project.name, name) 71 | self.assertIsInstance(project, packet.Project) 72 | 73 | def test_delete_project(self): 74 | project = self.manager.get_project("438659f0") 75 | self.assertIsNone(project.delete()) 76 | 77 | def test_list_devices(self): 78 | devices = self.manager.list_devices("438659f0") 79 | for device in devices: 80 | str(device) 81 | repr(device) 82 | self.assertIsInstance(device, packet.Device) 83 | 84 | # TODO figure out how to properly handle this test case 85 | # def test_list_all_devices(self): 86 | # devices = self.manager.list_all_devices("438659f0") 87 | # for device in devices: 88 | # str(device) 89 | # repr(device) 90 | # self.assertIsInstance(device, packet.Device) 91 | 92 | def test_create_device(self): 93 | device = self.manager.create_device( 94 | "438659f0", "hostname", "baremetal_0", "ewr1", "ubuntu_14_04" 95 | ) 96 | self.assertIsInstance(device, packet.Device) 97 | 98 | def test_create_device_ipxe(self): 99 | device = self.manager.create_device( 100 | "438659f0", 101 | "hostname", 102 | "baremetal_0", 103 | "ewr1", 104 | "custom_ipxe", 105 | ipxe_script_url="https://example.com", 106 | always_pxe=True, 107 | ) 108 | self.assertIsInstance(device, packet.Device) 109 | 110 | def test_get_device(self): 111 | device = self.manager.get_device("9dec7266") 112 | self.assertIsInstance(device, packet.Device) 113 | 114 | def test_device_actions(self): 115 | device = self.manager.get_device("9dec7266") 116 | self.assertIsNone(device.power_off()) 117 | self.assertIsNone(device.power_on()) 118 | self.assertIsNone(device.rescue()) 119 | self.assertIsNone(device.reboot()) 120 | 121 | def test_update_device(self): 122 | hostname = "updated hostname" 123 | device = self.manager.get_device("9dec7266") 124 | device.hostname = hostname 125 | device.update() 126 | self.assertEqual(device.hostname, hostname) 127 | self.assertIsInstance(device, packet.Device) 128 | 129 | def test_delete_device(self): 130 | device = self.manager.get_device("9dec7266") 131 | self.assertIsNone(device.delete()) 132 | 133 | def test_list_ssh_keys(self): 134 | keys = self.manager.list_ssh_keys() 135 | for key in keys: 136 | str(key) 137 | repr(key) 138 | self.assertIsInstance(key, packet.SSHKey) 139 | 140 | def test_get_ssh_key(self): 141 | key = self.manager.get_ssh_key("084a5dec") 142 | self.assertIsInstance(key, packet.SSHKey) 143 | 144 | def test_create_ssh_key(self): 145 | public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI4pIqzpb5g3992h+yr527VRcaB68KE4vPjWPPoiQws49KIs2NMcOzS9QE4641uW1u5ML2HgQdfYKMF/YFGnI1Y6xV637DjhDyZYV9LasUH49npSSJjsBcsk9JGfUpNAOdcgpFzK8V90eiOrOC5YncxdwwG8pwjFI9nNVPCl4hYEu1iXdyysHvkFfS2fklsNjLWrzfafPlaen+qcBxygCA0sFdW/7er50aJeghdBHnE2WhIKLUkJxnKadznfAge7oEe+3LLAPfP+3yHyvp2+H0IzmVfYvAjnzliYetqQ8pg5ZW2BiJzvqz5PebGS70y/ySCNW1qQmJURK/Wc1bt9en" 146 | 147 | key = self.manager.create_ssh_key(label="sshkey-name", public_key=public_key) 148 | self.assertIsInstance(key, packet.SSHKey) 149 | self.assertEqual(key.key, public_key) 150 | 151 | def test_delete_ssh_key(self): 152 | key = self.manager.get_ssh_key("084a5dec") 153 | self.assertIsNone(key.delete()) 154 | 155 | def test_update_ssh_key(self): 156 | label = "updated label" 157 | key = self.manager.get_ssh_key("084a5dec") 158 | key.label = label 159 | key.update() 160 | self.assertEqual(key.label, label) 161 | self.assertIsInstance(key, packet.SSHKey) 162 | 163 | def test_list_volumes(self): 164 | volumes = self.manager.list_volumes("438659f0") 165 | for volume in volumes: 166 | self.assertIsInstance(volume, packet.Volume) 167 | 168 | def test_create_volume(self): 169 | volume = self.manager.create_volume( 170 | "438659f0", "volume description", "storage_0", "100", "ewr1", 7, "1day" 171 | ) 172 | self.assertIsInstance(volume, packet.Volume) 173 | 174 | def test_get_volume(self): 175 | volume = self.manager.get_volume("f9a8a263") 176 | str(volume) 177 | repr(volume) 178 | self.assertIsInstance(volume, packet.Volume) 179 | 180 | def test_update_volume(self): 181 | description = "updated description" 182 | volume = self.manager.get_volume("f9a8a263") 183 | volume.description = description 184 | volume.update() 185 | self.assertEqual(volume.description, description) 186 | self.assertIsInstance(volume, packet.Volume) 187 | 188 | def test_delete_volume(self): 189 | volume = self.manager.get_volume("f9a8a263") 190 | self.assertIsNone(volume.delete()) 191 | 192 | def test_list_volume_snapshots(self): 193 | volume = self.manager.get_volume("f9a8a263") 194 | snaps = volume.list_snapshots() 195 | for snap in snaps: 196 | str(snap) 197 | repr(snap) 198 | snap.delete() 199 | 200 | def test_attach_volume(self): 201 | volume = self.manager.get_volume("f9a8a263") 202 | self.assertIsNone(volume.attach("9dec7266")) 203 | 204 | def test_detach_volume(self): 205 | volume = self.manager.get_volume("f9a8a263") 206 | self.assertIsNone(volume.detach()) 207 | 208 | def test_volume_create_snapshot(self): 209 | volume = self.manager.get_volume("f9a8a263") 210 | volume.create_snapshot() 211 | 212 | def test_volume_create_clone(self): 213 | volume = self.manager.get_volume("f9a8a263") 214 | volume.clone() 215 | 216 | def test_capacity(self): 217 | self.manager.get_capacity() 218 | 219 | def test_get_bgp_config(self): 220 | bgp = self.manager.get_bgp_config("1234") 221 | self.assertIsNotNone(bgp) 222 | 223 | def test_validate_capacity(self): 224 | capacity = self.manager.validate_capacity([("ewr1", "baremetal_0", 10)]) 225 | self.assertTrue(capacity) 226 | 227 | def test_validate_metro_capacity(self): 228 | capacity = self.manager.validate_metro_capacity([("sv", "baremetal_1", 10)]) 229 | self.assertTrue(capacity) 230 | 231 | # IP Addresses 232 | def test_list_device_ips(self): 233 | ips = self.manager.list_device_ips("e123s") 234 | self.assertIsNotNone(ips) 235 | 236 | def test_list_projects_ips(self): 237 | ips = self.manager.list_project_ips("438659f0") 238 | self.assertIsNotNone(ips) 239 | for ip in ips: 240 | self.assertIsInstance(ip.facility, packet.Facility) 241 | 242 | def test_list_projects_ips_state_all(self): 243 | ips = self.manager.list_project_ips("438659f1", params={"state": "all"}) 244 | self.assertIsNotNone(ips) 245 | self.assertIsNone(ips[0].facility) 246 | self.assertIsInstance(ips[1].facility, packet.Facility) 247 | 248 | 249 | class PacketMockManager(packet.Manager): 250 | def call_api(self, method, type="GET", params=None): 251 | with requests_mock.Mocker() as m: 252 | mock = { 253 | "DELETE": m.delete, 254 | "GET": m.get, 255 | "PUT": m.put, 256 | "POST": m.post, 257 | "PATCH": m.patch, 258 | }[type] 259 | 260 | if type == "DELETE": 261 | mock(requests_mock.ANY) 262 | return super(PacketMockManager, self).call_api(method, type, params) 263 | 264 | fixture = "%s_%s" % (type.lower(), method.lower()) 265 | fixture = fixture.replace("/", "_").split("?")[0] 266 | fixture = "test/fixtures/%s.json" % fixture 267 | 268 | headers = {"content-type": "application/json"} 269 | 270 | with open(fixture) as data_file: 271 | j = json.load(data_file) 272 | 273 | mock(requests_mock.ANY, headers=headers, json=j) 274 | return super(PacketMockManager, self).call_api(method, type, params) 275 | 276 | 277 | if __name__ == "__main__": 278 | sys.exit(unittest.main()) 279 | -------------------------------------------------------------------------------- /test/test_ports.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import time 7 | import unittest 8 | import packet 9 | 10 | from datetime import datetime 11 | 12 | 13 | @unittest.skipIf( 14 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 15 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 16 | ) 17 | class TestPorts(unittest.TestCase): 18 | @classmethod 19 | def setUpClass(self): 20 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 21 | 22 | org_id = self.manager.list_organizations()[0].id 23 | self.project = self.manager.create_organization_project( 24 | org_id=org_id, 25 | name="Int-Tests-Ports_{}".format( 26 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 27 | ), 28 | ) 29 | 30 | self.device = self.manager.create_device( 31 | self.project.id, "networktestingdevice", "baremetal_2", "ewr1", "centos_7" 32 | ) 33 | self.vlan = self.manager.create_vlan(self.project.id, "ewr1") 34 | self.vlan2 = self.manager.create_vlan(self.project.id, "ewr1") 35 | 36 | while True: 37 | if self.manager.get_device(self.device.id).state == "active": 38 | break 39 | time.sleep(2) 40 | self.device_port_id = self.device["network_ports"][0]["id"] 41 | self.device_eth0_port_id = self.device["network_ports"][1]["id"] 42 | 43 | def test01_convert_layer2(self): 44 | self.manager.convert_layer_2(self.device_port_id, self.vlan.id) 45 | 46 | def test02_remove_port(self): 47 | self.manager.remove_port(self.device_port_id, self.vlan.id) 48 | 49 | def test03_assign_port(self): 50 | self.manager.assign_port(self.device_port_id, self.vlan.id) 51 | 52 | def test04_bond_port(self): 53 | self.manager.bond_ports(self.device_port_id, False) 54 | 55 | def test05_disbond_port(self): 56 | self.manager.disbond_ports(self.device_port_id, False) 57 | 58 | def test06_assign_native_vlan(self): 59 | # must remove vlan from any previous association and attach more than one vlan to the eth0 port to be able to 60 | # choose a native vlan 61 | self.manager.remove_port(self.device_port_id, self.vlan.id) 62 | self.manager.assign_port(self.device_eth0_port_id, self.vlan.id) 63 | self.manager.assign_port(self.device_eth0_port_id, self.vlan2.id) 64 | self.manager.assign_native_vlan(self.device_eth0_port_id, self.vlan.id) 65 | 66 | def test07_remove_native_vlan(self): 67 | self.manager.remove_native_vlan(self.device_eth0_port_id) 68 | 69 | def test08_convert_layer3(self): 70 | ipadresses = list({"address_family": 6, "public": False}) 71 | self.manager.convert_layer_3(self.device_port_id, ipadresses) 72 | 73 | @classmethod 74 | def tearDownClass(self): 75 | self.manager.remove_port(self.device_eth0_port_id, self.vlan.id) 76 | self.device.delete() 77 | self.vlan2.delete() 78 | self.vlan.delete() 79 | self.project.delete() 80 | 81 | 82 | if __name__ == "__main__": 83 | sys.exit(unittest.main()) 84 | -------------------------------------------------------------------------------- /test/test_vlan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import time 7 | import unittest 8 | import packet 9 | 10 | from datetime import datetime 11 | 12 | 13 | @unittest.skipIf( 14 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 15 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 16 | ) 17 | class TestVlan(unittest.TestCase): 18 | @classmethod 19 | def setUpClass(self): 20 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 21 | 22 | org_id = self.manager.list_organizations()[0].id 23 | self.project = self.manager.create_organization_project( 24 | org_id=org_id, 25 | name="Int-Tests-VLAN_{}".format( 26 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 27 | ), 28 | ) 29 | 30 | self.device = self.manager.create_device( 31 | self.project.id, "vlantesting", "baremetal_2", "ewr1", "centos_7" 32 | ) 33 | 34 | self.vlan = self.manager.create_vlan(self.project.id, "ewr1") 35 | self.vlan2 = self.manager.create_vlan(self.project.id, "ewr1") 36 | while True: 37 | if self.manager.get_device(self.device.id).state == "active": 38 | break 39 | time.sleep(2) 40 | self.device_port_id = self.device["network_ports"][0]["id"] 41 | self.device_eth0_port_id = self.device["network_ports"][1]["id"] 42 | # must convert to layer 2 to work with vlans 43 | self.manager.convert_layer_2(self.device_port_id, self.vlan.id) 44 | 45 | def test_list_vlan(self): 46 | vlans = self.manager.list_vlans(self.project.id) 47 | self.assertTrue(len(vlans) > 0) 48 | 49 | def test_get_vlan(self): 50 | vlan = self.vlan.get() 51 | self.assertEqual(vlan["id"], self.vlan.id) 52 | 53 | def test_assign_port(self): 54 | self.manager.disbond_ports(self.device_eth0_port_id, False) 55 | self.manager.remove_port(self.device_port_id, self.vlan.id) 56 | self.manager.assign_port(self.device_eth0_port_id, self.vlan.id) 57 | self.manager.assign_port(self.device_eth0_port_id, self.vlan2.id) 58 | self.vlan.assign_native_vlan(self.device_eth0_port_id) 59 | 60 | def test_remove_port(self): 61 | self.vlan.remove_native_vlan(self.device_eth0_port_id) 62 | 63 | @classmethod 64 | def tearDownClass(self): 65 | self.device.delete() 66 | self.vlan.delete() 67 | self.vlan2.delete() 68 | self.project.delete() 69 | 70 | 71 | if __name__ == "__main__": 72 | sys.exit(unittest.main()) 73 | -------------------------------------------------------------------------------- /test/test_volume.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import time 8 | import packet 9 | 10 | from datetime import datetime 11 | 12 | 13 | @unittest.skipIf( 14 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 15 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 16 | ) 17 | class TestVolume(unittest.TestCase): 18 | @classmethod 19 | def setUpClass(self): 20 | self.timestamp = "" 21 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 22 | self.projectId = self.manager.list_projects()[0].id 23 | 24 | org_id = self.manager.list_organizations()[0].id 25 | self.project = self.manager.create_organization_project( 26 | org_id=org_id, 27 | name="Int-Tests-Volume_{}".format( 28 | datetime.utcnow().strftime("%Y%m%dT%H%M%S.%f")[:-3] 29 | ), 30 | ) 31 | 32 | self.volume = self.manager.create_volume( 33 | self.project.id, "volume description", "storage_1", "100", "ewr1", 7, "1day" 34 | ) 35 | 36 | while True: 37 | if self.manager.get_volume(self.volume.id).state == "active": 38 | break 39 | time.sleep(2) 40 | 41 | self.device = self.manager.create_device( 42 | self.project.id, "devicevolumestest", "baremetal_0", "ewr1", "centos_7" 43 | ) 44 | 45 | self.policy = self.volume.create_snapshot_policy("1day", 2) 46 | self.clone = self.volume.clone() 47 | 48 | while True: 49 | if self.manager.get_device(self.device.id).state == "active": 50 | break 51 | time.sleep(2) 52 | 53 | def test_get_volume(self): 54 | volume = self.manager.get_volume(self.volume.id) 55 | self.assertEqual(volume.description, self.volume.description) 56 | 57 | def test_list_volumes(self): 58 | volumes = self.manager.list_volumes(self.project.id) 59 | for volume in volumes: 60 | if volume.id is self.volume.id: 61 | break 62 | self.assertRaises(TypeError) 63 | 64 | def test_update_volume(self): 65 | self.volume.description = "newdescription" 66 | self.volume.update() 67 | volume = self.manager.get_volume(self.volume.id) 68 | self.assertEqual(self.volume.description, volume.description) 69 | 70 | def test_attach_volume(self): 71 | self.volume.attach(self.device.id) 72 | while True: 73 | if self.manager.get_device(self.device.id).state == "active": 74 | break 75 | time.sleep(1) 76 | 77 | def test_detach_volume(self): 78 | self.volume.detach() 79 | while True: 80 | if self.manager.get_device(self.device.id).state == "active": 81 | break 82 | time.sleep(1) 83 | 84 | def test_create_snapshot(self): 85 | self.volume.create_snapshot() 86 | 87 | def test_get_snapshots(self): 88 | snapshots = self.manager.get_snapshots(self.volume.id) 89 | self.assertIsNotNone(snapshots) 90 | self.__class__.timestamp = snapshots[0].timestamp 91 | 92 | def test_update_snapshot_policy(self): 93 | self.policy = self.policy.update_snapshot_policy("1month", 3) 94 | assert self.policy.frequency == "1month" 95 | assert self.policy.count == 3 96 | 97 | def test_restore_volume(self): 98 | self.volume.restore(self.__class__.timestamp) 99 | 100 | @classmethod 101 | def tearDownClass(self): 102 | self.policy.delete() 103 | self.volume.delete() 104 | self.device.delete() 105 | self.clone.delete() 106 | self.project.delete() 107 | 108 | 109 | if __name__ == "__main__": 110 | sys.exit(unittest.main()) 111 | -------------------------------------------------------------------------------- /test/test_vpn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | from __future__ import print_function 5 | 6 | import os 7 | import sys 8 | import unittest 9 | import packet 10 | 11 | 12 | @unittest.skipIf( 13 | "PACKET_PYTHON_TEST_ACTUAL_API" not in os.environ, 14 | "PACKET_PYTHON_TEST_ACTUAL_API is missing from environment", 15 | ) 16 | class TestVpn(unittest.TestCase): 17 | @classmethod 18 | def setUpClass(self): 19 | self.manager = packet.Manager(auth_token=os.environ["PACKET_AUTH_TOKEN"]) 20 | self.manager.turn_on_vpn() 21 | 22 | # def test_get_vpn_config(self): 23 | # config = self.manager.get_vpn_configuration("ewr1") 24 | # print(config) 25 | 26 | @classmethod 27 | def tearDownClass(self): 28 | self.manager.turn_off_vpn() 29 | 30 | 31 | if __name__ == "__main__": 32 | sys.exit(unittest.main()) 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | [tox] 4 | 5 | envlist = py27,py38,py39,py310 6 | skip_missing_interpreters=True 7 | 8 | [tool:pytest] 9 | testpaths = test 10 | # addopts = 11 | 12 | [gh-actions] 13 | python = 14 | 2.7: py27 15 | 3.8: py38 16 | 3.9: py39 17 | 3.10: py310, mypy 18 | 19 | [testenv] 20 | usedevelop=True 21 | setenv = 22 | PACKET_AUTH_TOKEN = {env:PACKET_AUTH_TOKEN:} 23 | 24 | deps = 25 | pytest 26 | coverage 27 | pytest-cov 28 | requests_mock 29 | 30 | commands= 31 | py.test --cov-append --cov-report=term-missing --cov packet --cov-report xml:{envdir}/../../coverage.xml --junitxml=unittest_{envname}.xml {posargs:-vv} 32 | 33 | depends = 34 | {py39}: clean 35 | report: py39 36 | 37 | [testenv:report] 38 | deps = coverage 39 | skip_install = true 40 | commands = 41 | coverage report 42 | coverage xml:coverage.xml 43 | 44 | [testenv:clean] 45 | deps = coverage 46 | skip_install = true 47 | commands = coverage erase 48 | --------------------------------------------------------------------------------