make-azure 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- make_azure-0.4.0/.gitignore +19 -0
- make_azure-0.4.0/PKG-INFO +77 -0
- make_azure-0.4.0/README.md +63 -0
- make_azure-0.4.0/pyproject.toml +47 -0
- make_azure-0.4.0/src/make_azure/__init__.py +36 -0
- make_azure-0.4.0/src/make_azure/azure.py +113 -0
- make_azure-0.4.0/src/make_azure/vm.py +1318 -0
- make_azure-0.4.0/tests/conftest.py +46 -0
- make_azure-0.4.0/tests/test_vm.py +329 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.env
|
|
10
|
+
|
|
11
|
+
# wrangler caches its Pages upload state here (site/ is deployed by direct upload).
|
|
12
|
+
.wrangler/
|
|
13
|
+
|
|
14
|
+
# The VS Code extension in editors/vscode: `dist/` is above, and these two are what
|
|
15
|
+
# building a .vsix leaves behind. `npm install` is never run there -- the extension has
|
|
16
|
+
# no runtime dependency and both CLIs come from `npx --yes` -- so node_modules/ is
|
|
17
|
+
# listed to catch the day somebody tries.
|
|
18
|
+
node_modules/
|
|
19
|
+
*.vsix
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: make-azure
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A free-tier Azure VM for mkrun -- nearest region, DuckDNS name, nothing billed left behind; no az CLI.
|
|
5
|
+
Project-URL: Homepage, https://make.optersoft.com
|
|
6
|
+
Project-URL: Documentation, https://academy.optersoft.com/cloud/azure/virtual-machine
|
|
7
|
+
Author-email: "Optersoft, S.L." <david@optersoft.com>
|
|
8
|
+
License-Expression: MIT OR Apache-2.0
|
|
9
|
+
Keywords: azure,duckdns,free tier,mk,mkrun,virtual machine
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: azure-identity>=1.19
|
|
12
|
+
Requires-Dist: mkrun>=0.4.3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# make-azure
|
|
16
|
+
|
|
17
|
+
A **free-tier Azure virtual machine** for [mkrun](https://pypi.org/project/mkrun/): created in
|
|
18
|
+
the nearest region that will build it, optionally given a DuckDNS name, and deleted with
|
|
19
|
+
everything it made, so nothing is left billing.
|
|
20
|
+
|
|
21
|
+
Sign-in is Microsoft's `azure-identity`: a browser, MFA up front, tokens in the encrypted
|
|
22
|
+
system keychain. Everything else is plain Resource Manager HTTPS. There's no `az` CLI and
|
|
23
|
+
no `azure-mgmt-*` SDK.
|
|
24
|
+
|
|
25
|
+
**Documentation:** [Virtual machine — Optersoft Academy](https://academy.optersoft.com/cloud/azure/virtual-machine)
|
|
26
|
+
|
|
27
|
+
## From any shell
|
|
28
|
+
|
|
29
|
+
```console
|
|
30
|
+
$ uv tool install mkrun
|
|
31
|
+
$ mk --install azure # make-azure, beside mk
|
|
32
|
+
$ mk azure.login --tenant <tenant> # once; a browser opens
|
|
33
|
+
$ mk azure.account # which subscription, and is it a free one
|
|
34
|
+
$ mk azure.region # the nearest regions, and the one azure.vm uses
|
|
35
|
+
$ mk azure.vm box # Ubuntu 24.04, SSH open
|
|
36
|
+
$ mk azure.vm box --duckdns mybox # ...and mybox.duckdns.org points at it
|
|
37
|
+
$ mk azure.vm box --no-public-ip # private network only; avoids the billed IPv4
|
|
38
|
+
$ mk azure.vms
|
|
39
|
+
$ mk azure.dns box mybox
|
|
40
|
+
$ mk azure.delete box
|
|
41
|
+
$ mk --dry-run azure.vm box # the plan; no sign-in, nothing sent
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With no task file in the directory, `mk` runs the tasks of the packages installed with
|
|
45
|
+
`mk --install`. Inside a project that has a `Makefile.py`, the file decides what is loaded;
|
|
46
|
+
use `mk --global azure.vm box` there.
|
|
47
|
+
|
|
48
|
+
## In a task file
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# /// script
|
|
52
|
+
# dependencies = ["make-azure"]
|
|
53
|
+
# ///
|
|
54
|
+
from make_azure import azure
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What is free
|
|
58
|
+
|
|
59
|
+
The free account (and Azure for Students) covers 750 hours a month of `Standard_B2ats_v2`
|
|
60
|
+
(the default), `Standard_B2pts_v2` or `Standard_B1s`, and a 64 GiB P6 disk, for its first 12
|
|
61
|
+
months. A Standard public IPv4 is **billed**, about $3.65 a month: `--no-public-ip` leaves it
|
|
62
|
+
out. A subscription without free services is refused unless `--paid-ok` is given.
|
|
63
|
+
|
|
64
|
+
## Settings
|
|
65
|
+
|
|
66
|
+
| variable | what |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `DUCKDNS_TOKEN` | for `--duckdns` / `azure.dns`; a credential, so it's withheld and redacted |
|
|
69
|
+
| `SSH_PUBLIC_KEY` | the key itself (CI); else `~/.ssh/id_ed25519.pub` or `id_rsa.pub` |
|
|
70
|
+
| `AZURE_CLIENT_ID` + `AZURE_TENANT_ID` | sign in as an app, with `AZURE_FEDERATED_TOKEN` (CI OIDC), `AZURE_FEDERATED_TOKEN_FILE` or `AZURE_CLIENT_SECRET` |
|
|
71
|
+
|
|
72
|
+
They come from the environment or `~/.make/secrets.env`. The sign-in is remembered in
|
|
73
|
+
`~/.make/azure/login.json`; that file holds no secret.
|
|
74
|
+
|
|
75
|
+
## Licence
|
|
76
|
+
|
|
77
|
+
MIT OR Apache-2.0.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# make-azure
|
|
2
|
+
|
|
3
|
+
A **free-tier Azure virtual machine** for [mkrun](https://pypi.org/project/mkrun/): created in
|
|
4
|
+
the nearest region that will build it, optionally given a DuckDNS name, and deleted with
|
|
5
|
+
everything it made, so nothing is left billing.
|
|
6
|
+
|
|
7
|
+
Sign-in is Microsoft's `azure-identity`: a browser, MFA up front, tokens in the encrypted
|
|
8
|
+
system keychain. Everything else is plain Resource Manager HTTPS. There's no `az` CLI and
|
|
9
|
+
no `azure-mgmt-*` SDK.
|
|
10
|
+
|
|
11
|
+
**Documentation:** [Virtual machine — Optersoft Academy](https://academy.optersoft.com/cloud/azure/virtual-machine)
|
|
12
|
+
|
|
13
|
+
## From any shell
|
|
14
|
+
|
|
15
|
+
```console
|
|
16
|
+
$ uv tool install mkrun
|
|
17
|
+
$ mk --install azure # make-azure, beside mk
|
|
18
|
+
$ mk azure.login --tenant <tenant> # once; a browser opens
|
|
19
|
+
$ mk azure.account # which subscription, and is it a free one
|
|
20
|
+
$ mk azure.region # the nearest regions, and the one azure.vm uses
|
|
21
|
+
$ mk azure.vm box # Ubuntu 24.04, SSH open
|
|
22
|
+
$ mk azure.vm box --duckdns mybox # ...and mybox.duckdns.org points at it
|
|
23
|
+
$ mk azure.vm box --no-public-ip # private network only; avoids the billed IPv4
|
|
24
|
+
$ mk azure.vms
|
|
25
|
+
$ mk azure.dns box mybox
|
|
26
|
+
$ mk azure.delete box
|
|
27
|
+
$ mk --dry-run azure.vm box # the plan; no sign-in, nothing sent
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With no task file in the directory, `mk` runs the tasks of the packages installed with
|
|
31
|
+
`mk --install`. Inside a project that has a `Makefile.py`, the file decides what is loaded;
|
|
32
|
+
use `mk --global azure.vm box` there.
|
|
33
|
+
|
|
34
|
+
## In a task file
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# /// script
|
|
38
|
+
# dependencies = ["make-azure"]
|
|
39
|
+
# ///
|
|
40
|
+
from make_azure import azure
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What is free
|
|
44
|
+
|
|
45
|
+
The free account (and Azure for Students) covers 750 hours a month of `Standard_B2ats_v2`
|
|
46
|
+
(the default), `Standard_B2pts_v2` or `Standard_B1s`, and a 64 GiB P6 disk, for its first 12
|
|
47
|
+
months. A Standard public IPv4 is **billed**, about $3.65 a month: `--no-public-ip` leaves it
|
|
48
|
+
out. A subscription without free services is refused unless `--paid-ok` is given.
|
|
49
|
+
|
|
50
|
+
## Settings
|
|
51
|
+
|
|
52
|
+
| variable | what |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `DUCKDNS_TOKEN` | for `--duckdns` / `azure.dns`; a credential, so it's withheld and redacted |
|
|
55
|
+
| `SSH_PUBLIC_KEY` | the key itself (CI); else `~/.ssh/id_ed25519.pub` or `id_rsa.pub` |
|
|
56
|
+
| `AZURE_CLIENT_ID` + `AZURE_TENANT_ID` | sign in as an app, with `AZURE_FEDERATED_TOKEN` (CI OIDC), `AZURE_FEDERATED_TOKEN_FILE` or `AZURE_CLIENT_SECRET` |
|
|
57
|
+
|
|
58
|
+
They come from the environment or `~/.make/secrets.env`. The sign-in is remembered in
|
|
59
|
+
`~/.make/azure/login.json`; that file holds no secret.
|
|
60
|
+
|
|
61
|
+
## Licence
|
|
62
|
+
|
|
63
|
+
MIT OR Apache-2.0.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "make-azure"
|
|
3
|
+
# 0.4.0, not 0.1.0: 0.1-0.3 were published and then deleted, and PyPI never
|
|
4
|
+
# lets a filename be used twice.
|
|
5
|
+
version = "0.4.0"
|
|
6
|
+
description = "A free-tier Azure VM for mkrun -- nearest region, DuckDNS name, nothing billed left behind; no az CLI."
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
# Same dual licence as mkrun. The implementation came from optersoft's
|
|
10
|
+
# azure-virtual-machine (AGPL-3.0, Optersoft its only author), relicensed here.
|
|
11
|
+
license = "MIT OR Apache-2.0"
|
|
12
|
+
authors = [{ name = "Optersoft, S.L.", email = "david@optersoft.com" }]
|
|
13
|
+
keywords = ["azure", "virtual machine", "free tier", "duckdns", "mk", "mkrun"]
|
|
14
|
+
# azure-identity owns the sign-in (MSAL, the encrypted token cache) and nothing
|
|
15
|
+
# else: the API calls are plain REST through `make.http`. Not the azure-mgmt-*
|
|
16
|
+
# SDKs -- more packages and a slow import to wrap a handful of URLs. It is
|
|
17
|
+
# imported lazily, so `mk --list` never pays for it. mkrun 0.4.3 is the first
|
|
18
|
+
# that loads `mkrun.groups` entry points with no task file.
|
|
19
|
+
dependencies = ["mkrun>=0.4.3", "azure-identity>=1.19"]
|
|
20
|
+
|
|
21
|
+
# What lets `mk azure.vm` run with no task file, once `mk --install azure`
|
|
22
|
+
# has put this package beside mk. The value is the module that registers the group.
|
|
23
|
+
[project.entry-points."mkrun.groups"]
|
|
24
|
+
azure = "make_azure.azure"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/make_azure"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://make.optersoft.com"
|
|
35
|
+
Documentation = "https://academy.optersoft.com/cloud/azure/virtual-machine"
|
|
36
|
+
|
|
37
|
+
[tool.uv.sources]
|
|
38
|
+
mkrun = { workspace = true }
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
addopts = "-q"
|
|
46
|
+
|
|
47
|
+
# No [tool.ruff] here on purpose: every member shares the root config.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Azure tasks for mkrun -- the `azure` group.
|
|
2
|
+
|
|
3
|
+
One thing: a virtual machine on Azure's free tier, in the nearest region that
|
|
4
|
+
will build it, optionally with a DuckDNS name, and removed with everything it
|
|
5
|
+
made. Sign-in is azure-identity (a browser, MFA up front, an encrypted token
|
|
6
|
+
cache); the rest is plain Resource Manager REST through `make.http`.
|
|
7
|
+
|
|
8
|
+
From any shell, with no task file:
|
|
9
|
+
|
|
10
|
+
uv tool install mkrun
|
|
11
|
+
mk --install azure
|
|
12
|
+
mk azure.vm box
|
|
13
|
+
|
|
14
|
+
Or in a task file, where importing is what registers it:
|
|
15
|
+
|
|
16
|
+
# Makefile.py
|
|
17
|
+
from make_azure import azure
|
|
18
|
+
|
|
19
|
+
The group merges with a repo's own `azure.*` tasks -- groups are namespaces,
|
|
20
|
+
and this package claims only its seven names.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
__version__ = "0.4.0"
|
|
26
|
+
|
|
27
|
+
__all__ = ["azure", "vm"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def __getattr__(name: str):
|
|
31
|
+
"""Import on first access, keeping module scope import-free -- startup is a feature."""
|
|
32
|
+
if name in __all__:
|
|
33
|
+
import importlib
|
|
34
|
+
|
|
35
|
+
return importlib.import_module(f".{name}", __name__)
|
|
36
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""The `azure` group: a free-tier virtual machine, made and removed.
|
|
2
|
+
|
|
3
|
+
mk azure.login --tenant T sign in once (a browser opens)
|
|
4
|
+
mk azure.account which subscription, and whether it is free
|
|
5
|
+
mk azure.region the nearest regions, and the one azure.vm uses
|
|
6
|
+
mk azure.vm box Ubuntu 24.04, SSH open, in the nearest region
|
|
7
|
+
mk azure.vm box --duckdns mybox ...and mybox.duckdns.org points at it
|
|
8
|
+
mk azure.vms what the subscription has
|
|
9
|
+
mk azure.dns box mybox point mybox.duckdns.org at an existing VM
|
|
10
|
+
mk azure.delete box the VM and everything it made
|
|
11
|
+
mk --dry-run azure.vm box the plan; nothing is sent, no sign-in
|
|
12
|
+
|
|
13
|
+
The work is in `make_azure.vm`, imported inside each task so `mk --list` never
|
|
14
|
+
loads azure-identity. No `az` CLI is involved.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Annotated
|
|
20
|
+
|
|
21
|
+
from make import arg, group
|
|
22
|
+
|
|
23
|
+
__all__ = ["azure"]
|
|
24
|
+
|
|
25
|
+
azure = group("azure")
|
|
26
|
+
|
|
27
|
+
Subscription = Annotated[str, arg(help="subscription id or name, if the sign-in reaches several")]
|
|
28
|
+
Size = Annotated[str, arg(help="Standard_B2ats_v2 (default), Standard_B2pts_v2 or Standard_B1s")]
|
|
29
|
+
Origin = Annotated[str | None, arg(help="your position, LAT,LON; default: from the time zone")]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@azure.task(name="login")
|
|
33
|
+
def login(
|
|
34
|
+
*,
|
|
35
|
+
tenant: Annotated[str, arg(help="tenant id or domain; needed for a personal account")] = "organizations",
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Sign in with a browser, MFA included; tokens go in the encrypted cache."""
|
|
38
|
+
from . import vm
|
|
39
|
+
|
|
40
|
+
vm.login(tenant)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@azure.task(name="account")
|
|
44
|
+
def account(*, subscription: Subscription = "") -> None:
|
|
45
|
+
"""The subscription this sign-in acts in, and whether its offer carries free services."""
|
|
46
|
+
from . import vm
|
|
47
|
+
|
|
48
|
+
vm.account(subscription_name=subscription)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@azure.task(name="region")
|
|
52
|
+
def region(
|
|
53
|
+
*, size: Size = "Standard_B2ats_v2", origin: Origin = None, subscription: Subscription = ""
|
|
54
|
+
) -> None:
|
|
55
|
+
"""The regions nearest to you, and the first that sells the size."""
|
|
56
|
+
from . import vm
|
|
57
|
+
|
|
58
|
+
vm.region(size=size, origin=origin, subscription_name=subscription)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@azure.task(name="vm", dangerous=True)
|
|
62
|
+
def create(
|
|
63
|
+
name: Annotated[str, arg(help="the VM's name; its resource group is NAME-rg")],
|
|
64
|
+
*,
|
|
65
|
+
size: Size = "Standard_B2ats_v2",
|
|
66
|
+
origin: Origin = None,
|
|
67
|
+
location: Annotated[str | None, arg(help="skip the search and use this region")] = None,
|
|
68
|
+
duckdns: Annotated[
|
|
69
|
+
str | None, arg(help="point SUBDOMAIN.duckdns.org at it (needs DUCKDNS_TOKEN)")
|
|
70
|
+
] = None,
|
|
71
|
+
user: Annotated[str, arg(help="the login user on the VM")] = "azureuser",
|
|
72
|
+
no_public_ip: Annotated[bool, arg(help="private network only; avoids the billed IPv4")] = False,
|
|
73
|
+
paid_ok: Annotated[bool, arg(help="allow a subscription without free services")] = False,
|
|
74
|
+
subscription: Subscription = "",
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Create a free-tier Ubuntu VM in the nearest region that will build it."""
|
|
77
|
+
from . import vm
|
|
78
|
+
|
|
79
|
+
vm.create(
|
|
80
|
+
name,
|
|
81
|
+
size=size,
|
|
82
|
+
origin=origin,
|
|
83
|
+
location=location,
|
|
84
|
+
duckdns=duckdns,
|
|
85
|
+
user=user,
|
|
86
|
+
paid_ok=paid_ok,
|
|
87
|
+
public_ip=not no_public_ip,
|
|
88
|
+
subscription_name=subscription,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@azure.task(name="vms")
|
|
93
|
+
def vms(*, subscription: Subscription = "") -> None:
|
|
94
|
+
"""The subscription's VMs: region, size, power state, address."""
|
|
95
|
+
from . import vm
|
|
96
|
+
|
|
97
|
+
vm.vms(subscription_name=subscription)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@azure.task(name="dns")
|
|
101
|
+
def dns(name: str, subdomain: str, *, subscription: Subscription = "") -> None:
|
|
102
|
+
"""Point SUBDOMAIN.duckdns.org at an existing VM (needs DUCKDNS_TOKEN)."""
|
|
103
|
+
from . import vm
|
|
104
|
+
|
|
105
|
+
vm.dns(name, subdomain, subscription_name=subscription)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@azure.task(name="delete", dangerous=True)
|
|
109
|
+
def delete(name: str, *, subscription: Subscription = "") -> None:
|
|
110
|
+
"""Delete a VM's resource group: the VM, its disk, network and IP, all at once."""
|
|
111
|
+
from . import vm
|
|
112
|
+
|
|
113
|
+
vm.delete(name, subscription_name=subscription)
|