├── .gitignore ├── README.md ├── examples ├── custom-image.nix ├── forwarding-rule │ ├── README.md │ └── nixops.nix ├── gce-route.nix ├── machine-with-disk.nix ├── rmq-gce.nix ├── simple-web-server │ └── nixops.nix └── trivial-gce.nix ├── nixops_gcp ├── __init__.py ├── backends │ ├── __init__.py │ ├── gce.py │ └── options.py ├── gcp_common.py ├── nix │ ├── common-gce-options.nix │ ├── default.nix │ ├── gce-credentials.nix │ ├── gce-disk.nix │ ├── gce-forwarding-rule.nix │ ├── gce-http-health-check.nix │ ├── gce-image.nix │ ├── gce-network.nix │ ├── gce-routes.nix │ ├── gce-static-ip.nix │ ├── gce-target-pool.nix │ ├── gce.nix │ ├── gse-bucket.nix │ └── image-options.nix ├── plugin.py └── resources │ ├── __init__.py │ ├── gce_disk.py │ ├── gce_forwarding_rule.py │ ├── gce_http_health_check.py │ ├── gce_image.py │ ├── gce_network.py │ ├── gce_route.py │ ├── gce_static_ip.py │ ├── gce_target_pool.py │ ├── gse_bucket.py │ └── types │ ├── __init__.py │ ├── gce_disk.py │ ├── gce_forwarding_rule.py │ ├── gce_http_health_check.py │ ├── gce_image.py │ ├── gce_network.py │ ├── gce_route.py │ ├── gce_static_ip.py │ ├── gce_target_pool.py │ └── gse_bucket.py ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── tests └── functional ├── single_machine_gce_base.nix └── single_machine_static_ip_gce.nix /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .coverage 3 | coverage.xml 4 | examples/*.json 5 | examples/*.lock 6 | html/ 7 | result 8 | tags 9 | tests/test.nixops* 10 | .mypy_cache 11 | 12 | syntax: glob 13 | .idea 14 | *egg-info 15 | *.log 16 | *.txt 17 | *.pyc 18 | *.swp 19 | *.csv 20 | *.out 21 | *.tar 22 | *.bkp 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NixOps backend for Google Cloud 2 | 3 | NixOps (formerly known as Charon) is a tool for deploying NixOS 4 | machines in a network or cloud. 5 | 6 | * [Manual](https://nixos.org/nixops/manual/) 7 | * [Installation](https://nixos.org/nixops/manual/#chap-installation) / [Hacking](https://nixos.org/nixops/manual/#chap-hacking) 8 | * [Continuous build](http://hydra.nixos.org/jobset/nixops/master#tabs-jobs) 9 | * [Source code](https://github.com/NixOS/nixops) 10 | * [Issue Tracker](https://github.com/NixOS/nixops/issues) 11 | * [Mailing list / Google group](https://groups.google.com/forum/#!forum/nixops-users) 12 | * [IRC - #nixos on freenode.net](irc://irc.freenode.net/#nixos) 13 | 14 | ## Developing 15 | 16 | To start developing on nixops, you can run: 17 | 18 | ```bash 19 | $ nix-shell 20 | $ poetry install 21 | $ poetry shell 22 | ``` 23 | To view active plugins: 24 | 25 | ```bash 26 | nixops list-plugins 27 | ``` 28 | 29 | and you're ready to go. Run `black`, `mypy`, etc. 30 | 31 | -------------------------------------------------------------------------------- /examples/custom-image.nix: -------------------------------------------------------------------------------- 1 | { 2 | gcpProject # (required) GCP project to deploy to 3 | , serviceAccount # (required) GCP service account email 4 | , accessKey # (required) path to GCP access key 5 | , region ? "us-east1-b" # GCE region 6 | , instanceType ? "n1-standard-2" # Default GCE VM (instance) type 7 | , subnet ? "" 8 | , volumeSize ? 50 # Default volume size 9 | , ... 10 | }: 11 | { 12 | network.description = "Deploy from custom image"; 13 | 14 | resources.gceImages.custom-image = 15 | { 16 | inherit serviceAccount; 17 | project = gcpProject; 18 | accessKey = builtins.readFile accessKey; 19 | sourceUri = "gs://nixos-cloud-images/nixos-image-18.09.1228.a4c4cbb613c-x86_64-linux.raw.tar.gz"; 20 | description = "custom 18.09 image"; 21 | }; 22 | 23 | 24 | machine = 25 | { resources, lib, ... }: 26 | { 27 | deployment.targetEnv = "gce"; 28 | deployment.gce = { 29 | inherit region subnet serviceAccount; 30 | project = gcpProject; 31 | instanceType = lib.mkDefault instanceType; 32 | accessKey = builtins.readFile accessKey; 33 | rootDiskSize = 10; 34 | bootstrapImage = resources.gceImages.custom-image; 35 | }; 36 | }; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /examples/forwarding-rule/README.md: -------------------------------------------------------------------------------- 1 | 2 | # `forwarding-rule` Example 3 | 4 | This is a network with a simple forwarding rule. 5 | It could use a bit of cleanup, but it's a more recent example than the other files, 6 | plus it's self-contained. Well, except for the credentials that is. 7 | -------------------------------------------------------------------------------- /examples/forwarding-rule/nixops.nix: -------------------------------------------------------------------------------- 1 | let 2 | credentials = { 3 | project = "nixops-testing"; 4 | serviceAccount = "deployer-tester@nixops-testing.iam.gserviceaccount.com"; 5 | accessKey = builtins.readFile ~/nixops-testing-0743723f3d6d.json; 6 | }; 7 | 8 | projectName = "nixops-testing"; 9 | regionName = "europe-west4"; 10 | region = "projects/${projectName}/regions/${regionName}"; 11 | zoneName = "${regionName}-c"; 12 | zone = "projects/${projectName}/zones/${zoneName}"; 13 | 14 | in 15 | { 16 | network.description = "GCE network"; 17 | 18 | # FIXME: use pin or flake for nixpkgs 19 | network.nixpkgs = import { overlays = [ (self: super: { python2 = super.python3; })]; }; 20 | network.storage.legacy = { 21 | databasefile = "~/.nixops/deployments-gce-example.nixops"; 22 | }; 23 | 24 | resources.gceNetworks.web = credentials // { 25 | firewall = { 26 | allow-http = { 27 | allowed.tcp = [ 80 ]; 28 | sourceRanges = ["0.0.0.0/0"]; 29 | }; 30 | allow-ssh = { 31 | allowed.tcp = [ 22 ]; 32 | sourceRanges = ["0.0.0.0/0"]; 33 | }; 34 | }; 35 | }; 36 | 37 | # resources.gceHTTPHealthChecks."httpHealthCheck" = credentials // { 38 | # name = "examples-fw-http-health-check"; 39 | # port = 80; 40 | # }; 41 | 42 | resources.gceTargetPools."httpTargetPool" = {nodes, resources, ...}: credentials // { 43 | name = "examples-fw-http-target-pool"; 44 | # healthCheck = resources.gceHTTPHealthChecks."httpHealthCheck"; 45 | machines = [ nodes.machine.config.deployment.gce.machineName ]; 46 | region = regionName; 47 | }; 48 | 49 | resources.gceForwardingRules."httpGceForwardingRules" = {resources, ...}: credentials // { 50 | # make name more specific? 51 | name = "examples-fw-http-gce-forwarding-rules"; 52 | protocol = "TCP"; 53 | targetPool = "examples-fw-http-target-pool"; 54 | description = "Web server forwarding rule"; 55 | portRange = "80"; 56 | region = regionName; 57 | }; 58 | 59 | machine = { pkgs, resources, ... }: 60 | { 61 | deployment.targetEnv = "gce"; 62 | deployment.gce = credentials // { 63 | # instance properties 64 | region = zoneName; 65 | instanceType = "n1-standard-2"; 66 | tags = ["crazy"]; 67 | scheduling.automaticRestart = true; 68 | scheduling.onHostMaintenance = "MIGRATE"; 69 | rootDiskSize = 20; 70 | network = resources.gceNetworks.web; 71 | }; 72 | 73 | documentation.enable = false; 74 | 75 | # fileSystems."/data"= 76 | # { autoFormat = true; 77 | # fsType = "ext4"; 78 | # gce.size = 10; 79 | # gce.disk_name = "data"; 80 | # }; 81 | 82 | services.nginx.enable = true; 83 | services.nginx.virtualHosts."localhost".root = pkgs.nix.doc + "/share/doc/nix/manual"; 84 | networking.firewall.allowedTCPPorts = [ 80 ]; 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /examples/gce-route.nix: -------------------------------------------------------------------------------- 1 | /** 2 | * A typical scenario where we want to route all traffic to an EC2 3 | * machine through a NAT Gateway in GCE, only allowing the NAT Gateway IP 4 | * in the security group of the EC2 instance. 5 | * 6 | * This mainly deploy two machines, one in EC2 "ec2machine" and the other 7 | * one in GCE "nat-instance". The goal is to route all traffic in the 8 | * default network in GCE the through "nat-instance". 9 | * 10 | * The deployment of the Security Group isn't included in this example. 11 | */ 12 | let 13 | region = "us-east-1"; 14 | zone = "us-east-1a"; 15 | in 16 | { 17 | nat-instance = 18 | { pkgs, resources, lib, ... }: 19 | { 20 | deployment.targetEnv = "gce"; 21 | deployment.gce = { 22 | canIpForward = true; 23 | region = "us-central1-a"; 24 | }; 25 | networking.nat.enable = true; 26 | }; 27 | 28 | resources.ec2KeyPairs.my-key-pair = 29 | { inherit region; }; 30 | 31 | ec2machine = 32 | { resources, lib, ... }: 33 | { 34 | deployment.targetEnv = "ec2"; 35 | deployment.ec2 = { 36 | inherit region zone; 37 | spotInstancePrice = 245; 38 | instanceType = "m4.large"; 39 | associatePublicIpAddress = true; 40 | keyPair = resources.ec2KeyPairs.my-key-pair ; 41 | }; 42 | }; 43 | 44 | resources.gceRoutes.lb-default = 45 | { resources, ... }: 46 | { 47 | destination = resources.machines.ec2machine; 48 | priority = 800; 49 | nextHop = resources.machines.nat-instance; 50 | tags = [ "worker" ] ; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /examples/machine-with-disk.nix: -------------------------------------------------------------------------------- 1 | { 2 | gcpProject # (required) GCP project to deploy to 3 | , serviceAccount # (required) GCP service account email 4 | , accessKey # (required) path to GCP access key 5 | , region ? "us-east1-b" # GCE region 6 | , instanceType ? "n1-standard-2" # Default GCE VM (instance) type 7 | , subnet ? "" 8 | , volumeSize ? 50 # Default volume size 9 | , labels ? {} 10 | , ... 11 | }: 12 | { 13 | network.description = "Machine + Disk example"; 14 | 15 | defaults = 16 | { resources, lib, uuid, ... }: 17 | { 18 | deployment.targetEnv = "gce"; 19 | deployment.gce = { 20 | inherit region subnet serviceAccount; 21 | project = gcpProject; 22 | labels = labels; 23 | instanceType = lib.mkDefault instanceType; 24 | accessKey = builtins.readFile accessKey; 25 | rootDiskSize = 10; 26 | }; 27 | 28 | fileSystems."/data" = { 29 | fsType = "xfs"; 30 | options = [ "noatime" "nodiratime" ]; 31 | autoFormat = true; 32 | formatOptions = "-f"; 33 | gce.disk = lib.mkDefault null; 34 | gce.disk_name = lib.mkDefault null; 35 | gce.size = lib.mkDefault volumeSize; 36 | gce.diskType = lib.mkDefault "ssd"; 37 | }; 38 | }; 39 | 40 | # GCE Disk for Frontend 41 | resources.gceDisks.frontend-volume = 42 | { resources, lib, ... }: 43 | let 44 | namespace = resources.machines.frontend.deployment.gce; 45 | in 46 | { 47 | inherit (namespace) project serviceAccount region accessKey labels; 48 | inherit (resources.machines.frontend.fileSystems."/data".gce) size disk diskType; 49 | name = "${namespace.machineName}-volume"; 50 | }; 51 | 52 | # Machine 53 | frontend = 54 | { resources, lib, ... }: 55 | { 56 | fileSystems."/data".gce.disk = resources.gceDisks.frontend-volume; 57 | fileSystems."/data".gce.disk_name = "volume"; 58 | }; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /examples/rmq-gce.nix: -------------------------------------------------------------------------------- 1 | /* 2 | This is an example RabbitMQ cluster application with PerfTest instances. 3 | 4 | RabbitMQ is CPU-bound, so it makes sense to run it on n1-highcpu-* instances. 5 | An instance of n1-highcpu-4 is capable of handling about 40k of "PerfTest -a" 6 | messages per second. 7 | 8 | About 5 PerfTest instances are required to load 3x n1-highcpu-4 cluster. 9 | Admin interface is at http://cluster_ip:15672 10 | 11 | Some deployment gotchas: 12 | rabbitmq has an autoclustering bug, which causes cluster to fail 13 | if all nodes are started at once. the code adds a startup delay 14 | for all nodes but the first one, but this is still unreliable. 15 | 16 | If node IP changes(eg due to start/stop/instance type change), 17 | it may fail to join the cluster without manual intervention. 18 | 19 | PerfTest nodes have 2 identical jobs because trying to use 20 | -x 2 -y 2 params to increase the number of threads in fact 21 | decreases the throughput(and causes 100% cpu utilization) 22 | probably due to scheduling issues. Separate processes also 23 | sometimes expose scheduling issues but not nearly as often 24 | and sometimes things settle by themselves after a warm-up 25 | period. 26 | 27 | there's no way to access load-balancer(cluster) ip in the 28 | deployment spec, so deployment has to happen in 2 steps. 29 | after the load-balancer is deployed, you have to manually 30 | replace amqp://rmq:123@146.148.2.203 with actual cluster url 31 | 32 | */ 33 | 34 | let 35 | pkgs = import {}; 36 | 37 | # change this as necessary or wipe and use ENV vars 38 | credentials = { 39 | project = "logicblox-dev"; 40 | serviceAccount = "572772620792-gecnc5v4ks9e6s13tociphd1p9ct6emr@developer.gserviceaccount.com"; 41 | accessKey = builtins.readFile "/home/freedom/nixos/phreedom/key.pem"; 42 | }; 43 | 44 | mkRabbitMQCluster = { prefix ? "rmq-", size, user ? "rmq", password, cookie, credentials, ipAddress ? null, region, extraConfig ? {}, extraGceConfig ? {} }: 45 | let 46 | cluster_node_names = map (id: "${prefix}${builtins.toString id}") 47 | ( pkgs.lib.range 0 (size - 1) ); 48 | master_node_name = builtins.head cluster_node_names; 49 | mkClusterNode = { name, master }: {resources, ...}: { 50 | services.rabbitmq = { 51 | enable = true; 52 | listenAddress = ""; 53 | plugins = [ "rabbitmq_management" "rabbitmq_management_visualiser" ]; 54 | inherit cookie; 55 | config = '' 56 | [ 57 | {rabbit, [ 58 | {default_user, <<"${user}">>}, 59 | {default_pass, <<"${password}">>}, 60 | 61 | {cluster_nodes, {[${ pkgs.lib.concatStringsSep "," (map (n: "'rabbit@${n}'") 62 | #(builtins.filter (n: n!=name) cluster_node_names)) 63 | cluster_node_names) 64 | }], disc}} 65 | ]}, 66 | %%{kernel, [ 67 | %% {inet_dist_listen_min, 10000}, 68 | %% {inet_dist_listen_max, 10005} 69 | %%]}, 70 | {rabbitmq_management, [{listener, [{port, 15672}]}]} 71 | %%{rabbitmq_management_agent, [ {force_fine_statistics, false} ] } 72 | ]. 73 | ''; 74 | }; 75 | networking.firewall.enable = false; #allowedTCPPorts = [ 5672 4369 25672 15672]; 76 | deployment.targetEnv = "gce"; 77 | deployment.gce = credentials // { 78 | tags = ["rmq-node" "rmq-manager" ]; 79 | network = resources.gceNetworks."${prefix}net"; 80 | } // extraGceConfig; 81 | } // (pkgs.lib.optionalAttrs ( !master ) { 82 | systemd.services.wait-first-node = { 83 | description = "Let the first node start to work around rmq bug"; 84 | wantedBy = [ "rabbitmq.service" ]; 85 | before = [ "rabbitmq.service" ]; 86 | script = '' 87 | sleep 10 88 | ''; 89 | serviceConfig.Type = "oneshot"; 90 | serviceConfig.RemainAfterExit = true; 91 | }; 92 | }) // extraConfig; 93 | 94 | cluster_nodes = pkgs.lib.fold pkgs.lib.mergeAttrs {} 95 | (map (name: { "${name}" = mkClusterNode { inherit name; master = (name == master_node_name);}; } ) 96 | cluster_node_names ); 97 | 98 | in { 99 | resources.gceHTTPHealthChecks."${prefix}hc" = credentials // { 100 | port = 15672; 101 | }; 102 | 103 | resources.gceTargetPools."${prefix}tp" = {resources, nodes, ...}: credentials // { 104 | healthCheck = resources.gceHTTPHealthChecks."${prefix}hc"; 105 | machines = map (name: nodes.${name}) cluster_node_names; # FIXME 106 | inherit region; 107 | }; 108 | 109 | resources.gceForwardingRules."${prefix}cluster" = {resources, ...}: credentials // { 110 | protocol = "TCP"; 111 | targetPool = resources.gceTargetPools."${prefix}tp"; 112 | description = "RabbitMQ cluster"; 113 | inherit ipAddress region; 114 | }; 115 | 116 | resources.gceNetworks."${prefix}net" = credentials // { 117 | firewall = { 118 | allow-rmq = { 119 | targetTags = ["rmq-node"]; 120 | allowed.tcp = [5672 4369 25672 "1000-65000" ]; 121 | }; 122 | allow-rmq-interface = { 123 | targetTags = [ "rmq-manager" ]; 124 | allowed.tcp = [15672]; 125 | }; 126 | }; 127 | }; 128 | 129 | } // cluster_nodes; 130 | 131 | # this merges attrs up to 2 levels deep to handle resources 132 | # will fail if there are multiple defs of the same machine 133 | mergeNetworks = n1: n2: 134 | n1 // n2 // (pkgs.lib.optionalAttrs ((n1 ? resources) && (n2 ? resources)) 135 | { resources = (pkgs.lib.mergeAttrsWithFunc pkgs.lib.mergeAttrs) n1.resources n2.resources; } 136 | ); 137 | joinNetworks = pkgs.lib.fold mergeNetworks {}; 138 | 139 | 140 | 141 | perftest_node_count = 5; 142 | perftest_node_names = map (id: "perftest-${builtins.toString id}") 143 | ( pkgs.lib.range 0 (perftest_node_count - 1) ); 144 | perftest_node = {pkgs, resources, ...}: { 145 | networking.firewall.enable = false; 146 | environment.systemPackages = [ pkgs.rabbitmq-java-client ]; 147 | deployment.targetEnv = "gce"; 148 | 149 | systemd.services.pt1 = { 150 | path = [ pkgs.rabbitmq-java-client ]; 151 | script = '' 152 | PerfTest -h amqp://rmq:123@146.148.2.203 -x 1 -y 1 -a 153 | ''; 154 | }; 155 | 156 | systemd.services.pt2 = { 157 | path = [ pkgs.rabbitmq-java-client ]; 158 | script = '' 159 | PerfTest -h amqp://rmq:123@146.148.2.203 -x 1 -y 1 -a 160 | ''; 161 | }; 162 | 163 | deployment.gce = credentials // { 164 | tags = ["rmq-node" "rmq-manager" ]; 165 | network = resources.gceNetworks.rmq-net; 166 | region = "europe-west1-b"; 167 | # instanceType = "f1-micro"; 168 | }; 169 | 170 | }; 171 | 172 | in 173 | 174 | 175 | joinNetworks [ 176 | (mkRabbitMQCluster { 177 | size = 3; 178 | password = "123"; 179 | inherit credentials; 180 | region = "europe-west1"; 181 | cookie = "jgnirughsdifgnsdkfgjnsdfj"; 182 | extraGceConfig = { 183 | region = "europe-west1-b"; 184 | instanceType = "n1-highcpu-4"; 185 | }; 186 | }) 187 | 188 | # PerfTest instances 189 | ( pkgs.lib.fold pkgs.lib.mergeAttrs {} 190 | (map (name: { "${name}" = perftest_node; } ) 191 | perftest_node_names ) ) 192 | 193 | ] 194 | -------------------------------------------------------------------------------- /examples/simple-web-server/nixops.nix: -------------------------------------------------------------------------------- 1 | let 2 | credentials = { 3 | project = "nixops-testing"; 4 | serviceAccount = "deployer-tester@nixops-testing.iam.gserviceaccount.com"; 5 | accessKey = builtins.readFile ~/nixops-testing-0743723f3d6d.json; 6 | }; 7 | in 8 | { 9 | network.description = "GCE network"; 10 | 11 | # FIXME: use pin or flake for nixpkgs 12 | network.nixpkgs = import { overlays = [ (self: super: { python2 = super.python3; })]; }; 13 | network.storage.legacy = { 14 | databasefile = "~/.nixops/deployments-gce-example.nixops"; 15 | } ; 16 | 17 | resources.gceNetworks.web = credentials // { 18 | firewall = { 19 | allow-http = { 20 | allowed.tcp = [ 80 ]; 21 | sourceRanges = ["0.0.0.0/0"]; 22 | }; 23 | allow-ssh = { 24 | allowed.tcp = [ 22 ]; 25 | sourceRanges = ["0.0.0.0/0"]; 26 | }; 27 | }; 28 | }; 29 | 30 | machine = { pkgs, resources, ... }: 31 | { deployment.targetEnv = "gce"; 32 | deployment.gce = credentials // { 33 | # instance properties 34 | region = "europe-west1-b"; 35 | instanceType = "n1-standard-2"; 36 | tags = ["crazy"]; 37 | scheduling.automaticRestart = true; 38 | scheduling.onHostMaintenance = "MIGRATE"; 39 | rootDiskSize = 20; 40 | network = resources.gceNetworks.web; 41 | }; 42 | 43 | # fileSystems."/data"= 44 | # { autoFormat = true; 45 | # fsType = "ext4"; 46 | # gce.size = 10; 47 | # gce.disk_name = "data"; 48 | # }; 49 | 50 | services.nginx.enable = true; 51 | services.nginx.virtualHosts."localhost".root = pkgs.nix.doc + "/share/doc/nix/manual"; 52 | networking.firewall.allowedTCPPorts = [ 80 ]; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /examples/trivial-gce.nix: -------------------------------------------------------------------------------- 1 | { 2 | machine = 3 | { deployment.targetEnv = "gce"; 4 | deployment.gce = { 5 | # credentials 6 | project = "..."; 7 | serviceAccount = "...@developer.gserviceaccount.com"; 8 | accessKey = builtins.readFile "/path/to/your.pem"; 9 | 10 | # instance properties 11 | region = "europe-west1-b"; 12 | instanceType = "n1-standard-2"; 13 | tags = ["crazy"]; 14 | scheduling.automaticRestart = true; 15 | scheduling.onHostMaintenance = "MIGRATE"; 16 | } ; 17 | 18 | fileSystems."/data"= 19 | { autoFormat = true; 20 | fsType = "ext4"; 21 | gce.size = 10; 22 | gce.encrypt = true; 23 | gce.disk_name = "data"; 24 | }; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /nixops_gcp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixops-gce/d13cb794aef763338f544010ceb1816fe31d7f42/nixops_gcp/__init__.py -------------------------------------------------------------------------------- /nixops_gcp/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixops-gce/d13cb794aef763338f544010ceb1816fe31d7f42/nixops_gcp/backends/__init__.py -------------------------------------------------------------------------------- /nixops_gcp/backends/options.py: -------------------------------------------------------------------------------- 1 | from nixops.resources import ( 2 | ResourceOptions, 3 | ResourceEval, 4 | ) 5 | from typing_extensions import Literal 6 | from typing import ( 7 | Sequence, 8 | Optional, 9 | Union, 10 | Mapping, 11 | ) 12 | 13 | 14 | class ImageOptions(ResourceOptions): 15 | name: Optional[str] 16 | family: Optional[str] 17 | project: Optional[str] 18 | 19 | 20 | class GCEDiskOptions(ResourceOptions): 21 | disk_name: Optional[str] 22 | disk: Optional[str] 23 | snapshot: Optional[str] 24 | image: ImageOptions 25 | size: Optional[int] 26 | diskType: Literal["standard", "ssd"] 27 | readOnly: bool 28 | bootDisk: bool 29 | deleteOnTermination: bool 30 | encrypt: bool 31 | cipher: str 32 | keySize: int 33 | passphrase: str 34 | 35 | 36 | class FilesystemsOptions(GCEDiskOptions): 37 | gce: GCEDiskOptions 38 | 39 | 40 | class SchedulingOptions(ResourceOptions): 41 | automaticRestart: bool 42 | onHostMaintenance: str 43 | preemptible: bool 44 | 45 | 46 | class InstanceserviceAccountOptions(ResourceOptions): 47 | email: str 48 | scopes: Sequence[str] 49 | 50 | 51 | class GceOptions(ResourceOptions): 52 | accessKey: str 53 | blockDeviceMapping: Mapping[str, GCEDiskOptions] 54 | bootstrapImage: ImageOptions 55 | canIpForward: bool 56 | instanceServiceAccount: InstanceserviceAccountOptions 57 | instanceType: str 58 | ipAddress: Optional[str] 59 | labels: Mapping[str, str] 60 | machineName: str 61 | metadata: Mapping[str, str] 62 | network: Optional[str] 63 | project: str 64 | region: str 65 | rootDiskSize: Optional[int] 66 | rootDiskType: str 67 | scheduling: SchedulingOptions 68 | serviceAccount: str 69 | subnet: Optional[str] 70 | tags: Sequence[str] 71 | fileSystems: Optional[Mapping[str, FilesystemsOptions]] 72 | -------------------------------------------------------------------------------- /nixops_gcp/gcp_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import re 5 | 6 | from nixops.util import attr_property 7 | import nixops.resources 8 | 9 | from libcloud.compute.types import Provider 10 | from libcloud.compute.providers import get_driver 11 | from libcloud.common.google import ( 12 | ResourceNotFoundError, 13 | GoogleBaseError, 14 | ) 15 | 16 | 17 | def optional_string(elem): 18 | return elem.get("value") if elem is not None else None 19 | 20 | 21 | def optional_int(elem): 22 | return int(elem.get("value")) if elem is not None else None 23 | 24 | 25 | def optional_bool(elem): 26 | return elem.get("value") == "true" if elem is not None else None 27 | 28 | 29 | def ensure_not_empty(value, name): 30 | if not value: 31 | raise Exception("{0} must not be empty".format(name)) 32 | 33 | 34 | def ensure_positive(value, name): 35 | if value <= 0: 36 | raise Exception("{0} must be a positive integer".format(name)) 37 | 38 | 39 | def retrieve_gce_image(_conn, img): 40 | """ 41 | Retrieve GCENodeImage based on family or name of the image 42 | Takes object as imageOptions submodule : {'project', 'name', 'family'} 43 | Returns the image object to be used for disks creation 44 | """ 45 | if img.name or img.family: 46 | # libcloud expects project to be empty list or a list of projects 47 | if not img.project: 48 | project = None 49 | else: 50 | project = [img.project] 51 | if img.family: 52 | """ Retrieve the latest image from the specified image family 53 | Optionally from a different project """ 54 | try: 55 | image = _conn.ex_get_image_from_family( 56 | image_family=img.family, 57 | ex_project_list=project, 58 | ex_standard_projects=False, 59 | ) 60 | except ResourceNotFoundError: 61 | raise Exception("Image family '{0}' was not found..".format(img.family)) 62 | except GoogleBaseError as ex: 63 | if ex.value["reason"] == "forbidden": 64 | raise Exception( 65 | "Image family '{0}' has not been made public in project '{1}'".format( 66 | img.family, img.project 67 | ) 68 | ) 69 | if ex.value["reason"] == "accessNotConfigured": 70 | raise Exception( 71 | "Project '{0}' does not exist or the Compute Engine API is disabled".format( 72 | img.project 73 | ) 74 | ) 75 | raise Exception(ex.value["message"]) 76 | else: 77 | """ Retrieve the image object using the name, partial name 78 | or full path of a GCE image, 79 | Optionally from a different project 80 | """ 81 | try: 82 | """ 83 | For image name, we need to specify the full image path because we cannot list images in a different project 84 | Ref : https://cloud.google.com/compute/docs/images/managing-access-custom-images#share-images-publicly 85 | Example : 86 | https://www.googleapis.com/compute/v1/projects/project-operations/global/images/nixos-18091228a4c4cbb613c-x86-64-linux 87 | """ 88 | image_full_path = img.name 89 | if project: 90 | image_full_path = "https://www.googleapis.com/compute/v1/projects/{prj}/global/images/{img}".format( 91 | prj=img.project, img=img.name 92 | ) 93 | image = _conn.ex_get_image( 94 | partial_name=image_full_path, 95 | ex_project_list=project, 96 | ex_standard_projects=False, 97 | ) 98 | except ResourceNotFoundError: 99 | raise Exception("Image '{0}' was not found..".format(img.name)) 100 | except GoogleBaseError as ex: 101 | if ex.value["reason"] == "forbidden": 102 | raise Exception( 103 | "Image '{0}' has not been made public in project '{1}'".format( 104 | img.name, img.project 105 | ) 106 | ) 107 | if ex.value["reason"] == "accessNotConfigured": 108 | raise Exception( 109 | "Project '{0}' does not exist or the Compute Engine API is disabled".format( 110 | img.project 111 | ) 112 | ) 113 | raise Exception(ex.value["message"]) 114 | return image 115 | if img.project: 116 | raise Exception( 117 | "Specify image name or image family alongside the project '{0}'..".format( 118 | img.project 119 | ) 120 | ) 121 | 122 | 123 | class ResourceDefinition(nixops.resources.ResourceDefinition): 124 | def __init__(self, name, config): 125 | super().__init__(name, config) 126 | 127 | res_name: str 128 | if hasattr(self.config, "gce") and hasattr(self.config.gce, "machineName"): 129 | res_name = self.config.gce.machineName 130 | else: 131 | res_name = self.config.name 132 | 133 | if ( 134 | len(res_name) > 63 135 | or re.match("[a-z]([-a-z0-9]{0,61}[a-z0-9])?$", res_name) is None 136 | ): 137 | raise Exception( 138 | "resource name ‘{0}‘ must be 1-63 characters long and " 139 | "match the regular expression [a-z]([-a-z0-9]*[a-z0-9])? which " 140 | "means the first character must be a lowercase letter, and all " 141 | "following characters must be a dash, lowercase letter, or digit, " 142 | "except the last character, which cannot be a dash. You may set a different name using the resource 'name' option.".format(res_name) 143 | ) 144 | 145 | if hasattr(self.config, "gce") and hasattr(self.config.gce, "project"): 146 | self.project = self.config.gce.project 147 | else: 148 | self.project = self.config.project 149 | if hasattr(self.config, "gce") and hasattr(self.config.gce, "serviceAccount"): 150 | self.service_account = self.config.gce.serviceAccount 151 | else: 152 | self.service_account = self.config.serviceAccount 153 | if hasattr(self.config, "gce") and hasattr(self.config.gce, "accessKey"): 154 | self.access_key_path = self.config.gce.accessKey 155 | else: 156 | self.access_key_path = self.config.accessKey 157 | 158 | 159 | class ResourceState(nixops.resources.ResourceState): 160 | 161 | project = attr_property("gce.project", None) 162 | service_account = attr_property("gce.serviceAccount", None) 163 | access_key_path = attr_property("gce.accessKey", None) 164 | 165 | def __init__(self, depl, name, id): 166 | nixops.resources.ResourceState.__init__(self, depl, name, id) 167 | self._conn = None 168 | 169 | def connect(self): 170 | if not self._conn: 171 | self._conn = get_driver(Provider.GCE)( 172 | self.service_account, self.access_key_path, project=self.project 173 | ) 174 | return self._conn 175 | 176 | @property 177 | def credentials_prefix(self): 178 | return "resources.{0}.$NAME".format(self.nix_name) 179 | 180 | def defn_project(self, defn): 181 | project = defn.project or os.environ.get("GCE_PROJECT") 182 | if not project: 183 | raise Exception( 184 | "please set '{0}.project' or $GCE_PROJECT".format( 185 | self.credentials_prefix 186 | ) 187 | ) 188 | return project 189 | 190 | def defn_service_account(self, defn): 191 | service_account = defn.service_account or os.environ.get("GCE_SERVICE_ACCOUNT") 192 | if not service_account: 193 | raise Exception( 194 | "please set '{0}.serviceAccount' or $GCE_SERVICE_ACCOUNT".format( 195 | self.credentials_prefix 196 | ) 197 | ) 198 | return service_account 199 | 200 | def defn_access_key_path(self, defn): 201 | access_key_path = defn.access_key_path or os.environ.get("ACCESS_KEY_PATH") 202 | if not access_key_path: 203 | raise Exception( 204 | "please set '{0}.accessKey' or $ACCESS_KEY_PATH".format( 205 | self.credentials_prefix 206 | ) 207 | ) 208 | return access_key_path 209 | 210 | def copy_credentials(self, defn): 211 | self.project = self.defn_project(defn) 212 | self.service_account = self.defn_service_account(defn) 213 | self.access_key_path = self.defn_access_key_path(defn) 214 | 215 | def is_deployed(self): 216 | return self.state == self.UP 217 | 218 | def no_change(self, condition, property_name): 219 | if self.is_deployed() and condition: 220 | raise Exception( 221 | "cannot change the {0} of a deployed {1}".format( 222 | property_name, self.full_name 223 | ) 224 | ) 225 | 226 | def no_property_change(self, defn, name): 227 | self.no_change( 228 | getattr(self, name) != getattr(defn, name), name.replace("_", " ") 229 | ) 230 | 231 | def no_project_change(self, defn): 232 | self.no_change(self.project != self.defn_project(defn), "project") 233 | 234 | def no_region_change(self, defn): 235 | self.no_change(self.region != defn.region, "region") 236 | 237 | def warn_missing_resource(self): 238 | if self.state == self.UP: 239 | self.warn( 240 | "{0} is supposed to exist, but is missing; recreating...".format( 241 | self.full_name 242 | ) 243 | ) 244 | self.state = self.MISSING 245 | 246 | def confirm_destroy(self, resource, res_name, abort=True): 247 | if self.depl.logger.confirm( 248 | "are you sure you want to destroy {0}?".format(res_name) 249 | ): 250 | self.log("destroying...") 251 | resource.destroy() 252 | return True 253 | else: 254 | if abort: 255 | raise Exception("can't proceed further") 256 | else: 257 | return False 258 | 259 | def warn_if_changed( 260 | self, expected_state, actual_state, name, resource_name=None, can_fix=True 261 | ): 262 | if expected_state != actual_state: 263 | self.warn( 264 | "{0} {1} has changed to '{2}'; expected it to be '{3}'{4}".format( 265 | resource_name or self.full_name, 266 | name, 267 | actual_state, 268 | expected_state, 269 | "" if can_fix else "; cannot fix this automatically", 270 | ) 271 | ) 272 | return actual_state 273 | 274 | # use warn_if_changed for a very typical use case of dealing 275 | # with changed properties which are stored in attributes 276 | # with user-friendly names 277 | def handle_changed_property( 278 | self, name, actual_state, property_name=None, can_fix=True 279 | ): 280 | self.warn_if_changed( 281 | getattr(self, name), 282 | actual_state, 283 | property_name or name.replace("_", " "), 284 | can_fix=can_fix, 285 | ) 286 | if can_fix: 287 | setattr(self, name, actual_state) 288 | 289 | def warn_not_supposed_to_exist( 290 | self, resource_name=None, valuable_data=False, valuable_resource=False 291 | ): 292 | valuables = " or ".join( 293 | [ 294 | d 295 | for d in [valuable_data and "data", valuable_resource and "resource"] 296 | if d 297 | ] 298 | ) 299 | valuable_msg = ( 300 | "; however, this also could be a resource name collision, " 301 | "and valuable {0} could be lost; before proceeding, " 302 | "please ensure that this isn't so".format(valuables) 303 | if valuables 304 | else "" 305 | ) 306 | self.warn( 307 | "{0} exists, but isn't supposed to; probably, this is the result " 308 | "of a botched creation attempt and can be fixed by deletion{1}".format( 309 | resource_name or self.full_name, valuable_msg 310 | ) 311 | ) 312 | 313 | # API to handle copying properties from definition to state 314 | # after resource is created or updated and checking that 315 | # the state is out of sync with the definition 316 | def copy_properties(self, defn): 317 | for attr in self.defn_properties: 318 | setattr(self, attr, getattr(defn, attr)) 319 | 320 | def properties_changed(self, defn): 321 | return any( 322 | getattr(self, attr) != getattr(defn, attr) for attr in self.defn_properties 323 | ) 324 | -------------------------------------------------------------------------------- /nixops_gcp/nix/common-gce-options.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | with lib; 3 | { 4 | labels = mkOption { 5 | default = { }; 6 | example = { foo = "bar"; xyzzy = "bla"; }; 7 | type = types.attrsOf types.str; 8 | description = '' 9 | A set of key/value label pairs to assign to the instance. 10 | ''; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /nixops_gcp/nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config_exporters = { optionalAttrs, ... }: [ 3 | (config: { 4 | gce = optionalAttrs (config.deployment.targetEnv == "gce") config.deployment.gce; 5 | }) 6 | ]; 7 | options = [ 8 | ./gce.nix 9 | ]; 10 | resources = { evalResources, zipAttrs, resourcesByType, ...}: { 11 | gceDisks = evalResources ./gce-disk.nix (zipAttrs resourcesByType.gceDisks or []); 12 | gceStaticIPs = evalResources ./gce-static-ip.nix (zipAttrs resourcesByType.gceStaticIPs or []); 13 | gceNetworks = evalResources ./gce-network.nix (zipAttrs resourcesByType.gceNetworks or []); 14 | gceHTTPHealthChecks = evalResources ./gce-http-health-check.nix (zipAttrs resourcesByType.gceHTTPHealthChecks or []); 15 | gceTargetPools = evalResources ./gce-target-pool.nix (zipAttrs resourcesByType.gceTargetPools or []); 16 | gceForwardingRules = evalResources ./gce-forwarding-rule.nix (zipAttrs resourcesByType.gceForwardingRules or []); 17 | gseBuckets = evalResources ./gse-bucket.nix (zipAttrs resourcesByType.gseBuckets or []); 18 | gceImages = evalResources ./gce-image.nix (zipAttrs resourcesByType.gceImages or []); 19 | gceRoutes = evalResources ./gce-routes.nix (zipAttrs resourcesByType.gceRoutes or []); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-credentials.nix: -------------------------------------------------------------------------------- 1 | lib: name: 2 | with lib; 3 | { 4 | 5 | serviceAccount = mkOption { 6 | default = ""; 7 | example = "12345-asdf@developer.gserviceaccount.com"; 8 | type = types.str; 9 | description = '' 10 | The GCE Service Account Email. If left empty, it defaults to the 11 | contents of the environment variable GCE_SERVICE_ACCOUNT. 12 | ''; 13 | }; 14 | 15 | accessKey = mkOption { 16 | default = ""; 17 | example = ./key.pem; 18 | type = types.either types.str types.path; 19 | description = '' 20 | The path to GCE Service Account key. If left empty, it defaults to the 21 | contents of the environment variable ACCESS_KEY_PATH. 22 | ''; 23 | }; 24 | 25 | project = mkOption { 26 | default = ""; 27 | example = "myproject"; 28 | type = types.str; 29 | description = '' 30 | The GCE project which should own the ${name}. If left empty, it defaults to the 31 | contents of the environment variable GCE_PROJECT. 32 | ''; 33 | }; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-disk.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | let 6 | imageOptions = import ./image-options.nix; 7 | in 8 | { 9 | 10 | options = (import ./gce-credentials.nix lib "disk") // { 11 | 12 | name = mkOption { 13 | example = "big-fat-disk"; 14 | default = "n-${shorten_uuid uuid}-${name}"; 15 | type = types.str; 16 | description = "Description of the GCE disk. This is the Name tag of the disk."; 17 | }; 18 | 19 | region = mkOption { 20 | example = "europe-west1-b"; 21 | type = types.str; 22 | description = "The GCE datacenter in which the disk should be created."; 23 | }; 24 | 25 | size = mkOption { 26 | default = null; 27 | example = 100; 28 | type = types.nullOr types.int; 29 | description = '' 30 | Disk size (in gigabytes). This may be left unset if you are 31 | creating the disk from a snapshot or image, in which case the 32 | size of the disk will be equal to the size of the snapshot or image. 33 | You can set a size larger than the snapshot or image, 34 | allowing the disk to be larger than the snapshot from which it is 35 | created. 36 | ''; 37 | }; 38 | 39 | snapshot = mkOption { 40 | default = null; 41 | example = "snap-1cbda474"; 42 | type = types.nullOr types.str; 43 | description = '' 44 | The snapshot name from which this disk will be created. If 45 | not specified, an empty disk is created. Changing the 46 | snapshot name has no effect if the disk already exists. 47 | ''; 48 | }; 49 | 50 | image = mkOption { 51 | default = {}; 52 | example = { name = null; family = "super-family"; project = "operations"; }; 53 | type = with types; either (resource "gce-image") (submodule imageOptions); 54 | description = '' 55 | The image, image family or image-resource from which to create the GCE disk. 56 | If not specified, an empty disk is created. Changing the 57 | image name has no effect if the disk already exists. 58 | ''; 59 | }; 60 | 61 | diskType = mkOption { 62 | default = "standard"; 63 | type = types.addCheck types.str 64 | (v: elem v [ "standard" "ssd" ]); 65 | description = '' 66 | The disk storage type (standard/ssd). 67 | ''; 68 | }; 69 | 70 | }; 71 | 72 | config = 73 | (mkAssert ( (config.snapshot == null) || ((config.image.name == null) && (config.image.family == null))) 74 | "Disk can not be created from both a snapshot, image name or image family at once" 75 | (mkAssert ( (config.size != null) || (config.snapshot != null) || (config.image.name != null) 76 | || (config.image.family != null) ) 77 | "Disk size is required unless it is created from an image or snapshot" { 78 | _type = "gce-disk"; 79 | } 80 | )); 81 | 82 | } 83 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-forwarding-rule.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | 6 | { 7 | 8 | options = (import ./gce-credentials.nix lib "forwarding rule") // { 9 | 10 | name = mkOption { 11 | example = "my-public-ip"; 12 | default = "n-${shorten_uuid uuid}-${name}"; 13 | type = types.str; 14 | description = "Description of the GCE Forwarding Rule. This is the Name tag of the rule."; 15 | }; 16 | 17 | region = mkOption { 18 | example = "europe-west1"; 19 | type = types.str; 20 | description = "The GCE region to which the forwarding rule should belong."; 21 | }; 22 | 23 | ipAddress = mkOption { 24 | default = null; 25 | example = "resources.gceStaticIPs.exampleIP"; 26 | type = types.nullOr ( types.either types.str (resource "gce-static-ip") ); 27 | description = '' 28 | GCE Static IP address resource to bind to or the name of 29 | an IP address not managed by NixOps. If left unset, 30 | an ephemeral(random) IP address will be assigned on deployment. 31 | ''; 32 | }; 33 | 34 | publicIPv4 = mkOption { 35 | default = null; 36 | type = types.uniq (types.nullOr types.str); 37 | description = '' 38 | The assigned IP address of this forwarding rule. 39 | This is set by NixOps to the ephemeral IP address of the resource if 40 | ipAddress wasn't set, otherwise it should be the same as ipAddress. 41 | ''; 42 | }; 43 | 44 | protocol = mkOption { 45 | example = "TCP"; 46 | type = types.addCheck types.str 47 | (v: elem v [ "AH" "ESP" "SCTP" "TCP" "UDP" ]); 48 | description = '' 49 | The IP protocol to which this rule applies. 50 | 51 | Acceptable values are: 52 | "AH": Specifies the IP Authentication Header protocol. 53 | "ESP": Specifies the IP Encapsulating Security Payload protocol. 54 | "SCTP": Specifies the Stream Control Transmission Protocol. 55 | "TCP": Specifies the Transmission Control Protocol. 56 | "UDP": Specifies the User Datagram Protocol. 57 | ''; 58 | }; 59 | 60 | targetPool = mkOption { 61 | example = "resources.gceStaticIPs.exampleIP"; 62 | type = types.either types.str (resource "gce-target-pool"); 63 | description = '' 64 | GCE Target Pool resource to receive the matched traffic 65 | or the name of a target pool not managed by NixOps. 66 | ''; 67 | }; 68 | 69 | portRange = mkOption { 70 | default = null; 71 | example = "1-1000"; 72 | type = types.nullOr types.str; 73 | description = '' 74 | If protocol is TCP or UDP, packets addressed to ports 75 | in the specified range will be forwarded to the target. 76 | 77 | Leave unset to forward all ports. 78 | ''; 79 | }; 80 | 81 | description = mkOption { 82 | default = null; 83 | example = "load balancer for the public site"; 84 | type = types.nullOr types.str; 85 | description = "An optional textual description of the Fowarding Rule."; 86 | }; 87 | 88 | }; 89 | 90 | config._type = "gce-forwarding-rule"; 91 | 92 | } 93 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-http-health-check.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | { 6 | 7 | options = (import ./gce-credentials.nix lib "HTTP health check") // { 8 | 9 | name = mkOption { 10 | example = "my-health-check"; 11 | default = "n-${shorten_uuid uuid}-${name}"; 12 | type = types.str; 13 | description = "Description of the GCE HTTP Health Check. This is the Name tag of the health check."; 14 | }; 15 | 16 | description = mkOption { 17 | default = null; 18 | example = "health check for databases"; 19 | type = types.nullOr types.str; 20 | description = "An optional textual description of the HTTP Health Check."; 21 | }; 22 | 23 | host = mkOption { 24 | default = null; 25 | example = "healthcheckhost.org"; 26 | type = types.nullOr types.str; 27 | description = '' 28 | The value of the host header in the HTTP health check request. 29 | If left unset(default value), the public IP on behalf of which 30 | this health check is performed will be used. 31 | ''; 32 | }; 33 | 34 | path = mkOption { 35 | default = "/"; 36 | example = "/is_healthy"; 37 | type = types.str; 38 | description = "The request path of the HTTP health check request."; 39 | }; 40 | 41 | port = mkOption { 42 | default = 80; 43 | example = 8080; 44 | type = types.int; 45 | description = "The TCP port number for the HTTP health check request."; 46 | }; 47 | 48 | checkInterval = mkOption { 49 | default = 5; 50 | example = 20; 51 | type = types.int; 52 | description = "How often (in seconds) to send a health check."; 53 | }; 54 | 55 | timeout = mkOption { 56 | default = 5; 57 | example = 20; 58 | type = types.int; 59 | description = "How long (in seconds) to wait before claiming failure."; 60 | }; 61 | 62 | unhealthyThreshold = mkOption { 63 | default = 2; 64 | example = 4; 65 | type = types.int; 66 | description = "A so-far healthy VM will be marked unhealthy after this many consecutive failures."; 67 | }; 68 | 69 | healthyThreshold = mkOption { 70 | default = 2; 71 | example = 4; 72 | type = types.int; 73 | description = "An unhealthy VM will be marked healthy after this many consecutive successes."; 74 | }; 75 | 76 | }; 77 | 78 | config._type = "gce-http-health-check"; 79 | 80 | } 81 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-image.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | 6 | { 7 | 8 | options = (import ./gce-credentials.nix lib "image") // { 9 | 10 | name = mkOption { 11 | example = "my-bootstrap-image"; 12 | default = "n-${shorten_uuid uuid}-${name}"; 13 | type = types.str; 14 | description = "Description of the GCE image. This is the Name tag of the image."; 15 | }; 16 | 17 | sourceUri = mkOption { 18 | example = "gs://nixos-cloud-images/nixos-image-18.09.1228.a4c4cbb613c-x86_64-linux.raw.tar.gz"; 19 | type = types.str; 20 | description = "The full Google Cloud Storage URL where the disk image is stored."; 21 | }; 22 | 23 | description = mkOption { 24 | default = null; 25 | example = "bootstrap image for the DB node"; 26 | type = types.nullOr types.str; 27 | description = "An optional textual description of the image."; 28 | }; 29 | 30 | }; 31 | 32 | config._type = "gce-image"; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-network.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | 6 | let 7 | 8 | gceFirewallOptions = { config, ... }: { 9 | 10 | options = { 11 | 12 | sourceRanges = mkOption { 13 | default = null; 14 | example = [ "192.168.0.0/16" ]; 15 | type = types.nullOr (types.listOf types.str); 16 | description = '' 17 | The address blocks that this rule applies to, expressed in 18 | CIDR 19 | format. An inbound connection is allowed if either the range or the tag of the 20 | source matches the or . 21 | As a convenience, leaving this option unset is equivalent to setting it to [ "0.0.0.0/0" ]. 22 | ''; 23 | }; 24 | 25 | sourceTags = mkOption { 26 | default = []; 27 | example = [ "admin" ]; 28 | type = types.listOf types.str; 29 | description = '' 30 | A list of instance tags which this rule applies to. Can be set in addition to 31 | . 32 | An inbound connection is allowed if either the range or the tag of the 33 | source matches the or . 34 | 35 | Don't forget to set to [] or at least a more 36 | restrictive range because the default setting makes 37 | irrelevant. 38 | ''; 39 | }; 40 | 41 | targetTags = mkOption { 42 | default = []; 43 | example = [ "public-http" ]; 44 | type = types.listOf types.str; 45 | description = '' 46 | A list of instance tags indicating sets of instances located on the network which 47 | may make network connections as specified in . If no 48 | are specified, the firewall rule applies to all 49 | instances on the network. 50 | ''; 51 | }; 52 | 53 | allowed = mkOption { 54 | #default = {}; 55 | example = { tcp = [ 80 ]; icmp = null; }; 56 | type = types.attrsOf (types.nullOr (types.listOf (types.either types.str types.int) )); 57 | description = '' 58 | Allowed protocols and ports. Setting protocol to null for example "icmp = null" 59 | allows all connections made using the protocol to proceed."; 60 | ''; 61 | }; 62 | 63 | }; 64 | 65 | config = {}; 66 | 67 | }; 68 | 69 | in 70 | { 71 | 72 | options = (import ./gce-credentials.nix lib "network") // { 73 | 74 | name = mkOption { 75 | example = "my-custom-network"; 76 | default = "n-${shorten_uuid uuid}-${name}"; 77 | type = types.str; 78 | description = "Description of the GCE Network. This is the Name tag of the network."; 79 | }; 80 | 81 | firewall = mkOption { 82 | default = { 83 | allow-ssh.allowed.tcp = [ 22 ]; 84 | }; 85 | example = { 86 | allow-http = { 87 | sourceRanges = ["0.0.0.0/0"]; 88 | allowed.tcp = [ 80 ]; 89 | }; 90 | }; 91 | type = with types; attrsOf (submodule gceFirewallOptions); 92 | description = '' 93 | Firewall rules. 94 | ''; 95 | }; 96 | 97 | }; 98 | 99 | config = { 100 | _type = "gce-network"; 101 | firewall.allow-ssh.allowed.tcp = [ 22 ]; 102 | }; 103 | 104 | } 105 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-routes.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | { 6 | 7 | options = (import ./gce-credentials.nix lib "route") // { 8 | 9 | name = mkOption { 10 | example = "my-route"; 11 | type = types.str; 12 | default = "route-${uuid}-${name}"; 13 | description = "Name of the route"; 14 | }; 15 | 16 | description = mkOption { 17 | example = "my-custom-route"; 18 | default = null; 19 | type = types.nullOr types.str; 20 | description = "Textual description of the route"; 21 | }; 22 | 23 | network = mkOption { 24 | example = "my-custom-network"; 25 | default = "default"; 26 | type = types.str; 27 | description = '' 28 | Name of the network, defaults to default. 29 | ''; 30 | }; 31 | 32 | destination = mkOption { 33 | example = "1.1.1.1/32"; 34 | type = types.nullOr (types.either types.str (resource "machine")); 35 | apply = x: if x == null || (builtins.isString x) then x else "res-" + x._name; 36 | description = '' 37 | The destination IP range that this route applies to. If the 38 | destination IP of a packet falls in this range, it matches 39 | this route. 40 | ''; 41 | }; 42 | 43 | priority = mkOption { 44 | default = 1000; 45 | example = 800; 46 | type = types.int; 47 | description = '' 48 | Priority is used to break ties when there is more than one 49 | matching route of maximum length. 50 | ''; 51 | }; 52 | 53 | nextHop = mkOption { 54 | default = null; 55 | example = "NAT-gateway"; 56 | type = types.nullOr (types.either types.str (resource "machine")); 57 | apply = x: if x == null || (builtins.isString x) then x else "res-" + x._name; 58 | description = '' 59 | Next traffic hop, Leave it empty for the default internet gateway. 60 | ''; 61 | }; 62 | 63 | tags = mkOption { 64 | default = null; 65 | type = types.nullOr (types.listOf types.str); 66 | description = '' 67 | The route applies to all instances with any of these tags, 68 | or to all instances in the network if no tags are specified 69 | ''; 70 | }; 71 | }; 72 | 73 | config = { 74 | _type = "gce-route"; 75 | }; 76 | 77 | } 78 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-static-ip.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | 6 | { 7 | 8 | options = (import ./gce-credentials.nix lib "IP address") // { 9 | 10 | name = mkOption { 11 | example = "my-public-ip"; 12 | default = "n-${shorten_uuid uuid}-${name}"; 13 | type = types.str; 14 | description = "Description of the GCE static IP address. This is the Name tag of the address."; 15 | }; 16 | 17 | region = mkOption { 18 | example = "europe-west1"; 19 | type = types.str; 20 | description = "The GCE region to which the IP address should be bound."; 21 | }; 22 | 23 | ipAddress = mkOption { 24 | default = null; 25 | example = "123.123.123.123"; 26 | type = types.nullOr types.str; 27 | description = '' 28 | The specific ephemeral IP address to promote to a static one. 29 | 30 | This lets you permanently reserve an ephemeral address used 31 | by one of resources to preserve it across machine teardowns 32 | or reassign it to another resource. Changing value of, setting 33 | or unsetting this option has no effect once the address resource 34 | is deployed, thus you can't lose the static IP unless you 35 | explicitly destroy it. 36 | ''; 37 | }; 38 | 39 | publicIPv4 = mkOption { 40 | default = null; 41 | type = types.uniq (types.nullOr types.str); 42 | description = '' 43 | The static IP address assigned. 44 | This is set by NixOps to the ephemeral IP address of the resource if 45 | ipAddress wasn't set, otherwise it should be the same as ipAddress. 46 | ''; 47 | }; 48 | }; 49 | 50 | config._type = "gce-static-ip"; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce-target-pool.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | 6 | let 7 | 8 | machine = mkOptionType { 9 | name = "GCE machine"; 10 | check = x: x ? gce; 11 | merge = mergeOneOption; 12 | }; 13 | 14 | in 15 | { 16 | 17 | options = (import ./gce-credentials.nix lib "target pool") // { 18 | 19 | name = mkOption { 20 | example = "my-target-pool"; 21 | default = "n-${shorten_uuid uuid}-${name}"; 22 | type = types.str; 23 | description = "Description of the GCE Target Pool. This is the Name tag of the target pool."; 24 | }; 25 | 26 | region = mkOption { 27 | example = "europe-west1"; 28 | type = types.str; 29 | description = "The GCE region to where the GCE Target Pool instances should reside."; 30 | }; 31 | 32 | healthCheck = mkOption { 33 | default = null; 34 | example = "resources.gceHTTPHealthChecks.my-check"; 35 | type = types.nullOr (types.either types.str (resource "gce-http-health-check")); 36 | description = '' 37 | GCE HTTP Health Check resource or name of a HTTP Health Check resource not managed by NixOps. 38 | 39 | A member VM in this pool is considered healthy if and only if the 40 | specified health checks passes. Unset health check means all member 41 | virtual machines will be considered healthy at all times but the health 42 | status of this target pool will be marked as unhealthy to indicate that 43 | no health checks are being performed. 44 | ''; 45 | }; 46 | 47 | machines = mkOption { 48 | default = []; 49 | example = [ "machines.httpserver1" "machines.httpserver2" ]; 50 | type = types.listOf (types.either types.str machine); 51 | description = '' 52 | The list of machine resources or fully-qualified GCE Node URLs to add to this pool. 53 | ''; 54 | }; 55 | 56 | }; 57 | 58 | config._type = "gce-target-pool"; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gce.nix: -------------------------------------------------------------------------------- 1 | # Configuration specific to the Google Compute Engine backend. 2 | 3 | { config, lib, pkgs, name, uuid, resources, ... }: 4 | 5 | with lib; 6 | with import lib; 7 | 8 | let 9 | replaceStrings = lib.replaceStrings or lib.replaceChars; 10 | 11 | gce_dev_prefix = "/dev/disk/by-id/scsi-0Google_PersistentDisk_"; 12 | 13 | get_disk_name = cfg: 14 | if cfg.disk != null 15 | then cfg.disk.name or cfg.disk 16 | else "${config.deployment.gce.machineName}-${cfg.disk_name}"; 17 | 18 | mkDefaultDiskName = mountPoint: cfg: cfg // { 19 | disk_name = if (cfg.disk_name == null) && (cfg.disk == null) 20 | then replaceStrings ["/" "." "_"] ["-" "-" "-"] 21 | (substring 1 ((stringLength mountPoint) - 1) mountPoint) 22 | else cfg.disk_name; 23 | }; 24 | 25 | imageOptions = import ./image-options.nix; 26 | 27 | gceDiskOptions = { config, ... }: { 28 | 29 | options = { 30 | 31 | disk_name = mkOption { 32 | default = null; 33 | example = "machine-persistent-disk2"; 34 | type = types.nullOr types.str; 35 | description = '' 36 | Name of the GCE disk to create. 37 | ''; 38 | }; 39 | 40 | disk = mkOption { 41 | default = null; 42 | example = "resources.gceDisks.exampleDisk"; 43 | type = types.nullOr ( types.either types.str (resource "gce-disk") ); 44 | apply = x: if x == null || (builtins.isString x) then x else x.name; 45 | description = '' 46 | GCE Disk resource or name of a disk not managed by NixOps to be mounted. 47 | ''; 48 | }; 49 | 50 | snapshot = mkOption { 51 | default = null; 52 | example = "snapshot-432"; 53 | type = types.nullOr types.str; 54 | description = '' 55 | The snapshot name from which to create the GCE disk. If 56 | not specified, an empty disk is created. Changing the 57 | snapshot name has no effect if the disk already exists. 58 | ''; 59 | }; 60 | 61 | image = mkOption { 62 | default = {}; 63 | type = with types; nullOr (either (resource "gce-image") (submodule imageOptions)); 64 | description = '' 65 | The image, image family or image-resource from which to create the GCE disk. 66 | If not specified, an empty disk is created. Changing the 67 | image name has no effect if the disk already exists. 68 | ''; 69 | }; 70 | 71 | size = mkOption { 72 | default = null; 73 | type = types.nullOr types.int; 74 | description = '' 75 | Volume size (in gigabytes) for automatically created 76 | GCE disks. This may be left unset if you are 77 | creating the disk from a snapshot or image, in which case the 78 | size of the disk will be equal to the size of the snapshot or image. 79 | You can set a size larger than the snapshot or image, 80 | allowing the disk to be larger than the snapshot from which it is 81 | created. 82 | ''; 83 | }; 84 | 85 | diskType = mkOption { 86 | default = "standard"; 87 | type = types.addCheck types.str 88 | (v: elem v [ "standard" "ssd" ]); 89 | description = '' 90 | The disk storage type (standard/ssd). 91 | ''; 92 | }; 93 | 94 | readOnly = mkOption { 95 | default = false; 96 | type = types.bool; 97 | description = '' 98 | Should the disk be attached to the instance as read-only. 99 | ''; 100 | }; 101 | 102 | bootDisk = mkOption { 103 | default = false; 104 | type = types.bool; 105 | description = '' 106 | Should the instance boot from this disk. 107 | ''; 108 | }; 109 | 110 | deleteOnTermination = mkOption { 111 | type = types.bool; 112 | description = '' 113 | For automatically created GCE disks, determines whether the 114 | disk should be deleted on instance destruction. 115 | ''; 116 | }; 117 | 118 | # FIXME: remove the LUKS options eventually? 119 | 120 | encrypt = mkOption { 121 | default = false; 122 | type = types.bool; 123 | description = '' 124 | Whether the GCE disk should be encrypted using LUKS. 125 | ''; 126 | }; 127 | 128 | cipher = mkOption { 129 | default = "aes-cbc-essiv:sha256"; 130 | type = types.str; 131 | description = '' 132 | The cipher used to encrypt the disk. 133 | ''; 134 | }; 135 | 136 | keySize = mkOption { 137 | default = 128; 138 | type = types.int; 139 | description = '' 140 | The size of the encryption key. 141 | ''; 142 | }; 143 | 144 | passphrase = mkOption { 145 | default = ""; 146 | type = types.str; 147 | description = '' 148 | The passphrase (key file) used to decrypt the key to access 149 | the device. If left empty, a passphrase is generated 150 | automatically; this passphrase is lost when you destroy the 151 | machine or remove the volume, unless you copy it from 152 | NixOps's state file. Note that the passphrase is stored in 153 | the Nix store of the instance, so an attacker who gains 154 | access to the GCE disk or instance store that contains the 155 | Nix store can subsequently decrypt the encrypted volume. 156 | ''; 157 | }; 158 | 159 | }; 160 | 161 | config = 162 | (mkAssert ( (config.snapshot == null) || ((config.image.name == null) && (config.image.family == null))) 163 | "Disk can not be created from both a snapshot and an image at once" 164 | (mkAssert ( (config.size != null) || (config.snapshot != null) || (config.image.name != null) 165 | || (config.image.family != null) || (config.disk != null) ) 166 | "Disk size is required unless it is created from an image or snapshot" { 167 | # Automatically delete volumes that are automatically created. 168 | deleteOnTermination = mkDefault ( config.disk == null ); 169 | } 170 | )); 171 | 172 | }; 173 | 174 | fileSystemsOptions = { config, ... }: { 175 | options = { 176 | gce = mkOption { 177 | default = null; 178 | type = with types; (nullOr (submodule gceDiskOptions)); 179 | description = '' 180 | GCE disk to be attached to this mount point. This is 181 | shorthand for defining a separate 182 | 183 | attribute. 184 | ''; 185 | }; 186 | }; 187 | config = mkIf(config.gce != null) { 188 | device = mkDefault "${ 189 | if config.gce.encrypt then "/dev/mapper/" else gce_dev_prefix 190 | }${ 191 | get_disk_name (mkDefaultDiskName config.mountPoint config.gce) 192 | }"; 193 | }; 194 | }; 195 | 196 | nixosVersion = builtins.substring 0 5 (config.system.nixos.version or config.system.nixosVersion); 197 | 198 | images = import ; 199 | 200 | in 201 | { 202 | ###### interface 203 | 204 | options = { 205 | 206 | deployment.gce = (import ./gce-credentials.nix lib "instance") // { 207 | 208 | machineName = mkOption { 209 | default = "n-${shorten_uuid uuid}-${name}"; 210 | example = "custom-machine-name"; 211 | type = types.str; 212 | description = "The GCE Instance Name."; 213 | }; 214 | 215 | region = mkOption { 216 | example = "europe-west1-b"; 217 | type = types.str; 218 | description = '' 219 | The GCE datacenter in which the instance should be created. 220 | ''; 221 | }; 222 | 223 | instanceType = mkOption { 224 | default = "g1-small"; 225 | example = "n1-standard-1"; 226 | type = types.str; 227 | description = '' 228 | GCE instance type. See for a 230 | list of valid instance types. 231 | ''; 232 | }; 233 | 234 | tags = mkOption { 235 | default = [ ]; 236 | example = [ "random" "tags" ]; 237 | type = types.listOf types.str; 238 | description = '' 239 | Tags to assign to the instance. These can be used in firewall and 240 | networking rules and are additionally available as metadata. 241 | ''; 242 | }; 243 | 244 | labels = (import ./common-gce-options.nix { inherit lib; }).labels; 245 | 246 | metadata = mkOption { 247 | default = {}; 248 | example = { loglevel = "warn"; }; 249 | type = types.attrsOf types.str; 250 | description = '' 251 | Metadata to assign to the instance. These are available to the instance 252 | via the metadata server. Some metadata keys such as "startup-script" 253 | are reserved by GCE and can influence the instance. 254 | ''; 255 | }; 256 | 257 | ipAddress = mkOption { 258 | default = null; 259 | example = "resources.gceStaticIPs.exampleIP"; 260 | type = types.nullOr ( types.either types.str (resource "gce-static-ip") ); 261 | description = '' 262 | GCE Static IP address resource to bind to or the name of 263 | an IP address not managed by NixOps. 264 | ''; 265 | }; 266 | 267 | network = mkOption { 268 | default = null; 269 | example = "resources.gceNetworks.verySecureNetwork"; 270 | type = types.nullOr ( types.either types.str (resource "gce-network") ); 271 | apply = x: if builtins.elem (builtins.typeOf x) [ "string" "null" ] then x else x.name; 272 | description = '' 273 | The GCE Network to make the instance a part of. Can be either 274 | a gceNetworks resource or a name of a network not managed by NixOps. 275 | ''; 276 | }; 277 | 278 | subnet = mkOption { 279 | default = null; 280 | type = with types; nullOr str; 281 | description = '' 282 | Specifies the subnet that the instances will be part of. 283 | ''; 284 | }; 285 | 286 | canIpForward = mkOption { 287 | default = false; 288 | type = types.bool; 289 | description = '' 290 | Allows the instance to send and receive packets with non-matching destination or source IPs. 291 | ''; 292 | }; 293 | 294 | instanceServiceAccount = mkOption { 295 | default = {}; 296 | type = (types.submodule { 297 | options = { 298 | email = mkOption { 299 | default = "default"; 300 | type = types.str; 301 | description = '' 302 | Email address of the service account. 303 | If not given, Google Compute Engine default service account is used. 304 | ''; 305 | }; 306 | scopes = mkOption { 307 | default = []; 308 | type = types.listOf types.str; 309 | description = '' 310 | The list of scopes to be made available for this service account. 311 | ''; 312 | }; 313 | }; 314 | }); 315 | description = '' 316 | A service account with its specified scopes, authorized for this instance. 317 | ''; 318 | }; 319 | 320 | blockDeviceMapping = mkOption { 321 | default = { }; 322 | example = { "/dev/sda".image = "bootstrap-img"; "/dev/sdb".disk = "vol-d04895b8"; }; 323 | type = with types; attrsOf (submodule gceDiskOptions); 324 | description = '' 325 | Block device mapping. 326 | ''; 327 | }; 328 | 329 | # Using NixOs public GCE images by default 330 | bootstrapImage = mkOption { 331 | default = 332 | let 333 | image = images."${nixosVersion}" or images.latest; 334 | in { inherit (image) name project; }; 335 | type = with types; (either (resource "gce-image") (submodule imageOptions)); 336 | description = '' 337 | Bootstrap image out of which the root disks 338 | of the machines will be created. 339 | ''; 340 | }; 341 | 342 | rootDiskSize = mkOption { 343 | default = null; 344 | example = 200; 345 | type = types.nullOr types.int; 346 | description = '' 347 | Root disk size(in gigabytes). Leave unset to be 348 | the same as size. 349 | ''; 350 | }; 351 | 352 | rootDiskType = mkOption { 353 | default = "standard"; 354 | type = types.addCheck types.str 355 | (v: elem v [ "standard" "ssd" ]); 356 | description = '' 357 | The root disk storage type (standard/ssd). 358 | ''; 359 | }; 360 | 361 | scheduling.automaticRestart = mkOption { 362 | default = true; 363 | type = types.bool; 364 | description = '' 365 | Whether the Instance should be automatically restarted when it is 366 | terminated by Google Compute Engine (not terminated by user). 367 | ''; 368 | }; 369 | 370 | scheduling.onHostMaintenance = mkOption { 371 | default = "MIGRATE"; 372 | type = types.addCheck types.str 373 | (v: elem v [ "MIGRATE" "TERMINATE" ]); 374 | description = '' 375 | Defines the maintenance behavior for this instance. For more information, see . 377 | 378 | Allowed values are: "MIGRATE" to let GCE automatically migrate your 379 | instances out of the way of maintenance events and 380 | "TERMINATE" to allow GCE to terminate and restart the instance. 381 | ''; 382 | }; 383 | 384 | scheduling.preemptible = mkOption { 385 | default = false; 386 | type = types.bool; 387 | description = '' 388 | Whether the instance is preemptible. 389 | For more information, see . 391 | ''; 392 | }; 393 | 394 | }; 395 | 396 | fileSystems = mkOption { 397 | type = with types; loaOf (submodule fileSystemsOptions); 398 | }; 399 | 400 | }; 401 | 402 | ###### implementation 403 | 404 | config = mkIf (config.deployment.targetEnv == "gce") { 405 | nixpkgs.system = mkOverride 900 "x86_64-linux"; 406 | 407 | 408 | deployment.gce.blockDeviceMapping = { 409 | "${gce_dev_prefix}${config.deployment.gce.machineName}-root" = { 410 | image = config.deployment.gce.bootstrapImage; 411 | size = config.deployment.gce.rootDiskSize; 412 | diskType = config.deployment.gce.rootDiskType; 413 | bootDisk = true; 414 | disk_name = "root"; 415 | }; 416 | } // (listToAttrs 417 | (map (fs: let fsgce = mkDefaultDiskName fs.mountPoint fs.gce; in 418 | nameValuePair "${gce_dev_prefix}${get_disk_name fsgce}" fsgce 419 | ) 420 | (filter (fs: fs.gce != null) (attrValues config.fileSystems)))); 421 | 422 | deployment.autoLuks = 423 | let 424 | f = name: dev: nameValuePair (get_disk_name dev) 425 | { device = name; 426 | autoFormat = true; 427 | inherit (dev) cipher keySize passphrase; 428 | }; 429 | in mapAttrs' f (filterAttrs (name: dev: dev.encrypt) config.deployment.gce.blockDeviceMapping); 430 | 431 | }; 432 | } 433 | -------------------------------------------------------------------------------- /nixops_gcp/nix/gse-bucket.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, uuid, name, ... }: 2 | 3 | with lib; 4 | with import lib; 5 | 6 | let 7 | 8 | corsOptions = { config, ... }: { 9 | options = { 10 | 11 | maxAgeSeconds = mkOption { 12 | default = 3600; 13 | example = 360; 14 | type = types.nullOr types.int; 15 | description = '' 16 | The value, in seconds, to return in the Access-Control-Max-Age 17 | header used in preflight responses. 18 | ''; 19 | }; 20 | 21 | methods = mkOption { 22 | default = [ ]; 23 | example = [ "GET" "POST" ]; 24 | type = types.listOf types.str; 25 | description = '' 26 | The list of HTTP methods on which to include CORS response headers, 27 | (GET, OPTIONS, POST, etc). Note: "*" is permitted in the list, 28 | and means "any method". 29 | ''; 30 | }; 31 | 32 | origins = mkOption { 33 | default = [ ]; 34 | example = [ "http://example.org" ]; 35 | type = types.listOf types.str; 36 | description = '' 37 | The list of Origins eligible to receive CORS response headers. 38 | Note: "*" is permitted in the list, and means "any Origin". 39 | ''; 40 | }; 41 | 42 | responseHeaders = mkOption { 43 | default = [ ]; 44 | example = [ "FIXME" ]; 45 | type = types.listOf types.str; 46 | description = '' 47 | The list of HTTP headers other than the 48 | simple response headers 49 | to give permission for the user-agent to share across domains. 50 | ''; 51 | }; 52 | 53 | }; 54 | config = {}; 55 | }; 56 | 57 | lifecycleOptions = { config, ... }: { 58 | options = { 59 | 60 | action = mkOption { 61 | default = "Delete"; 62 | type = types.str; 63 | description = '' 64 | The action to perform when all conditions are met. 65 | Currently only "Delete" is supported by GCE. 66 | ''; 67 | }; 68 | 69 | conditions.age = mkOption { 70 | default = null; 71 | example = 365; 72 | type = types.nullOr types.int; 73 | description = '' 74 | This condition is satisfied when an object reaches the specified age (in days). 75 | ''; 76 | }; 77 | 78 | conditions.createdBefore = mkOption { 79 | default = null; 80 | example = "2013-01-10"; 81 | type = types.nullOr types.str; 82 | description = '' 83 | This condition is satisfied when an object is created 84 | before midnight of the specified date in UTC. 85 | ''; 86 | }; 87 | 88 | conditions.numberOfNewerVersions = mkOption { 89 | default = null; 90 | example = 3; 91 | type = types.nullOr types.int; 92 | description = '' 93 | Relevant only for versioned objects. If the value is N, 94 | this condition is satisfied when there are at least N versions 95 | (including the live version) newer than this version of the object. 96 | For live objects, the number of newer versions is considered to be 0. 97 | For the most recent archived version, the number of newer versions 98 | is 1 (or 0 if there is no live object), and so on. 99 | ''; 100 | }; 101 | 102 | conditions.isLive = mkOption { 103 | default = null; 104 | type = types.nullOr types.bool; 105 | description = '' 106 | Relevant only for versioned objects. If the value is true, 107 | this condition matches the live objects; if the value is false, 108 | it matches archived objects. 109 | ''; 110 | }; 111 | 112 | }; 113 | config = {}; 114 | }; 115 | 116 | in 117 | { 118 | 119 | options = (import ./gce-credentials.nix lib "bucket") // { 120 | 121 | name = mkOption { 122 | example = "my-bucket"; 123 | default = "n-${shorten_uuid uuid}-${name}"; 124 | type = types.str; 125 | description = "This is the Name tag of the bucket."; 126 | }; 127 | 128 | cors = mkOption { 129 | example = [ { 130 | maxAgeSeconds = 100; 131 | methods = [ "GET" "PUT" ]; 132 | origins = [ "http://site.com" "http://site.org" ]; 133 | responseHeaders = [ "header1" "header2" ]; 134 | } ]; 135 | default = []; 136 | type = with types; listOf (submodule corsOptions); 137 | description = '' 138 | Cross-Origin Resource Sharing 139 | configuration. 140 | ''; 141 | }; 142 | 143 | lifecycle = mkOption { 144 | example = [ { conditions.age = 40; } ]; 145 | default = []; 146 | type = with types; listOf (submodule lifecycleOptions); 147 | description = '' 148 | Object Lifecycle Configuration for the bucket contents. 149 | ''; 150 | }; 151 | 152 | logging.logBucket = mkOption { 153 | default = null; 154 | example = "resources.gseBuckets.logBucket"; 155 | type = types.nullOr ( types.either types.str (resource "gse-bucket") ); 156 | description = '' 157 | The destination bucket where the current bucket's logs should be placed. 158 | 159 | FIXME: is this a bucket name or a fully-qualified url? 160 | ''; 161 | }; 162 | 163 | logging.logObjectPrefix = mkOption { 164 | example = "log"; 165 | default = null; 166 | type = types.nullOr types.str; 167 | description = "A prefix for log object names."; 168 | }; 169 | 170 | location = mkOption { 171 | example = "EU"; 172 | default = "US"; 173 | type = types.str; 174 | description = '' 175 | Object data for objects in the bucket resides in physical storage 176 | within this region. Defaults to US. See the developer's guide for 177 | the authoritative list. 178 | ''; 179 | }; 180 | 181 | storageClass = mkOption { 182 | example = "DURABLE_REDUCED_AVAILABILITY"; 183 | default = "STANDARD"; 184 | type = types.str; 185 | description = '' 186 | This defines how objects in the bucket are stored and determines 187 | the SLA and the cost of storage. Typical values are STANDARD and 188 | DURABLE_REDUCED_AVAILABILITY. 189 | See the developer's guide for the authoritative list. 190 | ''; 191 | }; 192 | 193 | versioning.enabled = mkOption { 194 | default = false; 195 | type = types.bool; 196 | description = '' 197 | While set to true, versioning is fully enabled for this bucket. 198 | ''; 199 | }; 200 | 201 | website.mainPageSuffix = mkOption { 202 | example = "index.html"; 203 | default = null; 204 | type = types.nullOr types.str; 205 | description = '' 206 | Behaves as the bucket's directory index where missing 207 | objects are treated as potential directories. 208 | 209 | For example, with mainPageSuffix main_page_suffix configured to be index.html, 210 | a GET request for http://example.com would retrieve http://example.com/index.html, 211 | and a GET request for http://example.com/photos would 212 | retrieve http://example.com/photos/index.html. 213 | ''; 214 | }; 215 | 216 | website.notFoundPage = mkOption { 217 | example = "404.html"; 218 | default = null; 219 | type = types.nullOr types.str; 220 | description = '' 221 | Serve this object on request for a non-existent object. 222 | ''; 223 | }; 224 | 225 | }; 226 | 227 | config._type = "gse-bucket"; 228 | 229 | } 230 | -------------------------------------------------------------------------------- /nixops_gcp/nix/image-options.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | with lib; 4 | { 5 | 6 | options = { 7 | name = mkOption { 8 | default = null; 9 | example = "image-2cfda297"; 10 | type = types.nullOr types.str; 11 | description = '' 12 | Name of an existent image or image-resource to be used. 13 | Must specify the project if the image is defined as public. 14 | ''; 15 | }; 16 | 17 | family = mkOption { 18 | default = null; 19 | example = "nixos-20-03"; 20 | type = types.nullOr types.str; 21 | description = '' 22 | Image family to grab the latest non-deprecated image from. 23 | Must specify the project if the image family is defined as public. 24 | ''; 25 | }; 26 | 27 | project = mkOption { 28 | default = null; 29 | example = "gcp-project"; 30 | type = types.nullOr types.str; 31 | description = '' 32 | The parent project containing a GCE image that was made public 33 | for all authenticated users. 34 | ''; 35 | }; 36 | 37 | }; 38 | config = 39 | (mkAssert ( (config.name == null) || (config.family == null) ) 40 | "Must specify either image name or image family" 41 | (mkAssert ( (config.project != null) && ((config.name == null) 42 | || (config.family == null))) 43 | "Specify either image name or image family alongside the project" 44 | {} 45 | )); 46 | } 47 | -------------------------------------------------------------------------------- /nixops_gcp/plugin.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import nixops.plugins 3 | from nixops.plugins import Plugin 4 | 5 | 6 | class NixopsGCPPlugin(Plugin): 7 | @staticmethod 8 | def nixexprs(): 9 | return [os.path.dirname(os.path.abspath(__file__)) + "/nix"] 10 | 11 | @staticmethod 12 | def load(): 13 | return [ 14 | "nixops_gcp.resources", 15 | "nixops_gcp.backends.gce", 16 | ] 17 | 18 | 19 | @nixops.plugins.hookimpl 20 | def plugin(): 21 | return NixopsGCPPlugin() 22 | -------------------------------------------------------------------------------- /nixops_gcp/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from . import gce_disk 2 | from . import gce_forwarding_rule 3 | from . import gce_http_health_check 4 | from . import gce_image 5 | from . import gce_network 6 | from . import gce_route 7 | from . import gce_static_ip 8 | from . import gce_target_pool 9 | from . import gse_bucket 10 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_disk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Persistent Disks. 4 | 5 | import os 6 | import libcloud.common.google 7 | from libcloud.compute.types import Provider 8 | from libcloud.compute.providers import get_driver 9 | 10 | from nixops.util import attr_property 11 | from nixops_gcp.gcp_common import ( 12 | ResourceDefinition, 13 | ResourceState, 14 | retrieve_gce_image, 15 | optional_string, 16 | optional_int, 17 | ) 18 | from nixops_gcp.resources.gce_image import GCEImageState 19 | from .types.gce_disk import GceDiskOptions 20 | 21 | 22 | class GCEDiskDefinition(ResourceDefinition): 23 | """Definition of a GCE Persistent Disk""" 24 | 25 | config: GceDiskOptions 26 | 27 | @classmethod 28 | def get_type(cls): 29 | return "gce-disk" 30 | 31 | @classmethod 32 | def get_resource_type(cls): 33 | return "gceDisks" 34 | 35 | def __init__(self, name, config): 36 | super().__init__(name, config) 37 | self.disk_name = self.config.name 38 | self.region = self.config.region 39 | self.size = self.config.size 40 | self.snapshot = self.config.snapshot 41 | self.image = self.config.image 42 | self.disk_type = self.config.diskType 43 | 44 | def show_type(self): 45 | return "{0} [{1}]".format(self.get_type(), self.region) 46 | 47 | 48 | class GCEDiskState(ResourceState): 49 | """State of a GCE Persistent Disk""" 50 | 51 | region = attr_property("gce.region", None) 52 | size = attr_property("gce.size", None, int) 53 | disk_name = attr_property("gce.disk_name", None) 54 | disk_type = attr_property("gce.diskType", None) 55 | 56 | @classmethod 57 | def get_type(cls): 58 | return "gce-disk" 59 | 60 | def __init__(self, depl, name, id): 61 | ResourceState.__init__(self, depl, name, id) 62 | 63 | def show_type(self): 64 | s = super().show_type() 65 | if self.state == self.UP: 66 | s = "{0} [{1}; {2} GiB]".format(s, self.region, self.size) 67 | return s 68 | 69 | @property 70 | def resource_id(self): 71 | return self.disk_name 72 | 73 | nix_name = "gceDisks" 74 | 75 | @property 76 | def full_name(self): 77 | return "GCE disk '{0}'".format(self.disk_name) 78 | 79 | def disk(self): 80 | return self.connect().ex_get_volume(self.disk_name, self.region) 81 | 82 | def create(self, defn, check, allow_reboot, allow_recreate): 83 | self.no_change(defn.size and self.size != defn.size, "size") 84 | self.no_property_change(defn, "disk_type") 85 | self.no_project_change(defn) 86 | self.no_region_change(defn) 87 | 88 | self.copy_credentials(defn) 89 | self.disk_name = defn.disk_name 90 | 91 | if check: 92 | try: 93 | disk = self.disk() 94 | if self.state == self.UP: 95 | self.handle_changed_property( 96 | "region", disk.extra["zone"].name, can_fix=False 97 | ) 98 | self.handle_changed_property( 99 | "disk_type", disk.extra["type"][3:], can_fix=False 100 | ) 101 | self.handle_changed_property("size", int(disk.size), can_fix=False) 102 | else: 103 | self.warn_not_supposed_to_exist(valuable_data=True) 104 | self.confirm_destroy(disk, self.full_name) 105 | 106 | except libcloud.common.google.ResourceNotFoundError: 107 | self.warn_missing_resource() 108 | 109 | if self.state != self.UP: 110 | img = defn.image 111 | 112 | extra_msg = ( 113 | " from snapshot '{0}'".format(defn.snapshot) 114 | if defn.snapshot 115 | else " from image family '{0}'".format(img.family) 116 | if img.family 117 | else " from image '{0}'".format(img.name) 118 | if img.name 119 | else "" 120 | ) 121 | if img.project: 122 | extra_msg += " in project '{0}'".format(img.project) 123 | self.log( 124 | "creating GCE disk of {0} GiB{1}...".format( 125 | defn.size if defn.size else "auto", extra_msg 126 | ) 127 | ) 128 | 129 | if hasattr(img, "_type") and img._type == "gce-image": 130 | img = self.depl.active_resources.get(img._name).image() 131 | else: 132 | img = retrieve_gce_image(self.connect(), img=img) 133 | 134 | try: 135 | volume = self.connect().create_volume( 136 | size=defn.size, 137 | name=defn.disk_name, 138 | location=defn.region, 139 | snapshot=defn.snapshot, 140 | image=img, 141 | use_existing=False, 142 | ex_disk_type="pd-" + defn.disk_type, 143 | ex_image_family=None, 144 | ) 145 | except AttributeError: 146 | # libcloud bug: The region we're trying to create the disk 147 | # in doesn't exist. 148 | raise Exception( 149 | "tried creating a disk in nonexistent " "region %r" % v["region"] 150 | ) 151 | except libcloud.common.google.ResourceExistsError: 152 | raise Exception( 153 | "tried creating a disk that already exists; " 154 | "please run 'deploy --check' to fix this" 155 | ) 156 | except libcloud.common.google.ResourceNotFoundError: 157 | raise Exception( 158 | "The snapshot '{0}' to be used for the volume creation does not exist".format( 159 | defn.snapshot 160 | ) 161 | ) 162 | self.state = self.UP 163 | self.region = defn.region 164 | self.size = volume.size 165 | self.disk_type = defn.disk_type 166 | 167 | def destroy(self, wipe=False): 168 | if self.state == self.UP: 169 | try: 170 | return self.confirm_destroy(self.disk(), self.full_name, abort=False) 171 | except libcloud.common.google.ResourceNotFoundError: 172 | self.warn( 173 | "tried to destroy {0} which didn't exist".format(self.full_name) 174 | ) 175 | return True 176 | 177 | def create_after(self, resources, defn): 178 | return {r for r in resources if isinstance(r, GCEImageState)} 179 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_forwarding_rule.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Forwarding Rules 4 | 5 | import os 6 | import libcloud.common.google 7 | 8 | from typing import Optional 9 | from nixops_gcp.resources.gce_static_ip import GCEStaticIPState 10 | from nixops_gcp.resources.gce_target_pool import GCETargetPoolState 11 | from nixops.util import attr_property 12 | from nixops_gcp.gcp_common import ( 13 | ResourceDefinition, 14 | ResourceState, 15 | optional_string, 16 | ensure_not_empty, 17 | ) 18 | from .types.gce_forwarding_rule import GceForwardingRuleOptions 19 | 20 | 21 | class GCEForwardingRuleDefinition(ResourceDefinition): 22 | """Definition of a GCE Forwarding Rule""" 23 | 24 | config: GceForwardingRuleOptions 25 | 26 | @classmethod 27 | def get_type(cls): 28 | return "gce-forwarding-rule" 29 | 30 | @classmethod 31 | def get_resource_type(cls): 32 | return "gceForwardingRules" 33 | 34 | def __init__(self, name, config): 35 | super().__init__(name, config) 36 | 37 | self.forwarding_rule_name = self.config.name 38 | 39 | self.region = self.config.region 40 | self.protocol = self.config.protocol 41 | 42 | pr = self.config.portRange 43 | self.port_range = ( 44 | None if pr is None else "{0}-{1}".format(pr, pr) if pr.isdigit() else pr 45 | ) 46 | 47 | self.description = self.config.description 48 | self.target_pool = self.config.targetPool 49 | self.ip_address = self.config.ipAddress 50 | 51 | def show_type(self): 52 | return "{0} [{1}]".format(self.get_type(), self.region) 53 | 54 | 55 | class GCEForwardingRuleState(ResourceState): 56 | """State of a GCE Forwarding Rule""" 57 | 58 | forwarding_rule_name = attr_property("gce.name", None) 59 | target_pool = attr_property("gce.targetPool", None) 60 | region = attr_property("gce.region", None) 61 | protocol = attr_property("gce.protocol", None) 62 | port_range = attr_property("gce.portRange", None) 63 | ip_address = attr_property("gce.ipAddress", None) 64 | description = attr_property("gce.description", None) 65 | public_ipv4 = attr_property("gce.public_ipv4", None) 66 | 67 | @classmethod 68 | def get_type(cls): 69 | return "gce-forwarding-rule" 70 | 71 | def __init__(self, depl, name, id): 72 | ResourceState.__init__(self, depl, name, id) 73 | 74 | def show_type(self): 75 | s = super(GCEForwardingRuleState, self).show_type() 76 | if self.state == self.UP: 77 | s = "{0} [{1}]".format(s, self.region) 78 | return s 79 | 80 | def prefix_definition(self, attr): 81 | return {("resources", "gceForwardingRules"): attr} 82 | 83 | def get_physical_spec(self): 84 | return {"publicIPv4": self.public_ipv4} 85 | 86 | @property 87 | def resource_id(self): 88 | return self.forwarding_rule_name 89 | 90 | nix_name = "gceForwardingRules" 91 | 92 | @property 93 | def full_name(self): 94 | return "GCE forwarding rule '{0}'".format(self.forwarding_rule_name) 95 | 96 | def forwarding_rule(self): 97 | return self.connect().ex_get_forwarding_rule(self.forwarding_rule_name) 98 | 99 | defn_properties = [ 100 | "target_pool", 101 | "region", 102 | "protocol", 103 | "port_range", 104 | "ip_address", 105 | "description", 106 | ] 107 | 108 | def create(self, defn, check, allow_reboot, allow_recreate): 109 | self.no_property_change(defn, "target_pool") 110 | self.no_property_change(defn, "protocol") 111 | self.no_property_change(defn, "port_range") 112 | self.no_property_change(defn, "ip_address") 113 | self.no_property_change(defn, "description") 114 | self.no_project_change(defn) 115 | self.no_region_change(defn) 116 | 117 | self.copy_credentials(defn) 118 | self.forwarding_rule_name = defn.forwarding_rule_name 119 | 120 | if check: 121 | try: 122 | fwr = self.forwarding_rule() 123 | if self.state == self.UP: 124 | self.handle_changed_property( 125 | "public_ipv4", fwr.address, property_name="IP address" 126 | ) 127 | 128 | self.handle_changed_property( 129 | "region", fwr.region.name, can_fix=False 130 | ) 131 | self.handle_changed_property( 132 | "target_pool", fwr.targetpool.name, can_fix=False 133 | ) 134 | self.handle_changed_property( 135 | "protocol", fwr.protocol, can_fix=False 136 | ) 137 | self.handle_changed_property( 138 | "description", fwr.extra["description"], can_fix=False 139 | ) 140 | self.warn_if_changed( 141 | self.port_range or "1-65535", 142 | fwr.extra["portRange"], 143 | "port range", 144 | can_fix=False, 145 | ) 146 | 147 | if self.ip_address: 148 | try: 149 | address = self.connect().ex_get_address(self.ip_address) 150 | if self.public_ipv4 and self.public_ipv4 != address.address: 151 | self.warn( 152 | "static IP Address {0} assigned to this machine has unexpectely " 153 | "changed from {1} to {2} most likely due to being redeployed" 154 | "; cannot fix this automatically".format( 155 | self.ip_address, 156 | self.public_ipv4, 157 | address.address, 158 | ) 159 | ) 160 | 161 | except libcloud.common.google.ResourceNotFoundError: 162 | self.warn( 163 | "static IP Address resource {0} used by this forwarding rule has been destroyed; " 164 | "it is likely that the forwarding rule is still holding the address itself ({1}) " 165 | "and this is your last chance to reclaim it before it gets lost".format( 166 | self.ip_address, self.public_ipv4 167 | ) 168 | ) 169 | 170 | else: 171 | self.warn_not_supposed_to_exist() 172 | self.confirm_destroy(fwr, self.full_name) 173 | 174 | except libcloud.common.google.ResourceNotFoundError: 175 | self.warn_missing_resource() 176 | 177 | if self.state != self.UP: 178 | self.log("creating {0}...".format(self.full_name)) 179 | try: 180 | fwr = self.connect().ex_create_forwarding_rule( 181 | defn.forwarding_rule_name, 182 | defn.target_pool, 183 | region=defn.region, 184 | protocol=defn.protocol, 185 | port_range=defn.port_range, 186 | address=defn.ip_address, 187 | description=defn.description, 188 | ) 189 | except libcloud.common.google.ResourceExistsError: 190 | raise Exception( 191 | "tried creating a forwarding rule that already exists; " 192 | "please run 'deploy --check' to fix this" 193 | ) 194 | self.state = self.UP 195 | self.copy_properties(defn) 196 | self.public_ipv4 = fwr.address 197 | self.log("got IP: {0}".format(self.public_ipv4)) 198 | 199 | # only changing of target pool is supported by GCE, but not libcloud 200 | # FIXME: implement 201 | 202 | def destroy(self, wipe=False): 203 | if self.state == self.UP: 204 | try: 205 | fwr = self.forwarding_rule() 206 | return self.confirm_destroy(fwr, self.full_name, abort=False) 207 | except libcloud.common.google.ResourceNotFoundError: 208 | self.warn( 209 | "tried to destroy {0} which didn't exist".format(self.full_name) 210 | ) 211 | return True 212 | 213 | def create_after(self, resources, defn): 214 | return { 215 | r 216 | for r in resources 217 | if isinstance(r, GCETargetPoolState) or isinstance(r, GCEStaticIPState) 218 | } 219 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_http_health_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE HTTP Health Checks 4 | 5 | import os 6 | import libcloud.common.google 7 | 8 | from nixops.util import attr_property 9 | from nixops_gcp.gcp_common import ( 10 | ResourceDefinition, 11 | ResourceState, 12 | optional_string, 13 | ensure_not_empty, 14 | ensure_positive, 15 | ) 16 | from .types.gce_http_health_check import GceHttpHealthCheckOptions 17 | 18 | 19 | class GCEHTTPHealthCheckDefinition(ResourceDefinition): 20 | """Definition of a GCE HTTP Health Check""" 21 | 22 | config: GceHttpHealthCheckOptions 23 | 24 | @classmethod 25 | def get_type(cls): 26 | return "gce-http-health-check" 27 | 28 | @classmethod 29 | def get_resource_type(cls): 30 | return "gceHTTPHealthChecks" 31 | 32 | def __init__(self, name, config): 33 | super().__init__(name, config) 34 | self.healthcheck_name = self.config.name 35 | self.description = self.config.description 36 | self.host = self.config.host 37 | self.path = self.config.path 38 | self.port = self.config.port 39 | self.check_interval = self.config.checkInterval 40 | self.timeout = self.config.timeout 41 | self.unhealthy_threshold = self.config.unhealthyThreshold 42 | self.healthy_threshold = self.config.healthyThreshold 43 | 44 | def show_type(self): 45 | return "{0} [:{1}{2}]".format(self.get_type(), self.port, self.path) 46 | 47 | 48 | class GCEHTTPHealthCheckState(ResourceState): 49 | """State of a GCE HTTP Health Check""" 50 | 51 | healthcheck_name = attr_property("gce.name", None) 52 | host = attr_property("gce.host", None) 53 | path = attr_property("gce.path", None) 54 | port = attr_property("gce.port", None, int) 55 | description = attr_property("gce.description", None) 56 | check_interval = attr_property("gce.checkInterval", None, int) 57 | timeout = attr_property("gce.timeout", None, int) 58 | unhealthy_threshold = attr_property("gce.unhealthyThreshold", None, int) 59 | healthy_threshold = attr_property("gce.healthyThreshold", None, int) 60 | 61 | @classmethod 62 | def get_type(cls): 63 | return "gce-http-health-check" 64 | 65 | def __init__(self, depl, name, id): 66 | ResourceState.__init__(self, depl, name, id) 67 | 68 | def show_type(self): 69 | s = super(GCEHTTPHealthCheckState, self).show_type() 70 | if self.state == self.UP: 71 | s = "{0} [:{1}{2}]".format(s, self.port, self.path) 72 | return s 73 | 74 | @property 75 | def resource_id(self): 76 | return self.healthcheck_name 77 | 78 | nix_name = "gceHTTPHealthChecks" 79 | 80 | @property 81 | def full_name(self): 82 | return "GCE HTTP health check '{0}'".format(self.healthcheck_name) 83 | 84 | def healthcheck(self): 85 | return self.connect().ex_get_healthcheck(self.healthcheck_name) 86 | 87 | defn_properties = [ 88 | "host", 89 | "path", 90 | "port", 91 | "description", 92 | "check_interval", 93 | "timeout", 94 | "unhealthy_threshold", 95 | "healthy_threshold", 96 | ] 97 | 98 | def create(self, defn, check, allow_reboot, allow_recreate): 99 | self.no_project_change(defn) 100 | 101 | self.copy_credentials(defn) 102 | self.healthcheck_name = defn.healthcheck_name 103 | 104 | if check: 105 | try: 106 | hc = self.healthcheck() 107 | if self.state == self.UP: 108 | self.handle_changed_property("host", hc.extra["host"]) 109 | self.handle_changed_property("path", hc.path) 110 | self.handle_changed_property("port", hc.port) 111 | self.handle_changed_property("timeout", hc.timeout) 112 | self.handle_changed_property("description", hc.extra["description"]) 113 | self.handle_changed_property("check_interval", hc.interval) 114 | self.handle_changed_property( 115 | "healthy_threshold", hc.healthy_threshold 116 | ) 117 | self.handle_changed_property( 118 | "unhealthy_threshold", hc.unhealthy_threshold 119 | ) 120 | else: 121 | self.warn_not_supposed_to_exist() 122 | self.confirm_destroy(hc, self.full_name) 123 | 124 | except libcloud.common.google.ResourceNotFoundError: 125 | self.warn_missing_resource() 126 | 127 | if self.state != self.UP: 128 | self.log("creating {0}...".format(self.full_name)) 129 | try: 130 | healthcheck = self.connect().ex_create_healthcheck( 131 | defn.healthcheck_name, 132 | host=defn.host, 133 | path=defn.path, 134 | port=defn.port, 135 | interval=defn.check_interval, 136 | timeout=defn.timeout, 137 | unhealthy_threshold=defn.unhealthy_threshold, 138 | healthy_threshold=defn.healthy_threshold, 139 | description=defn.description, 140 | ) 141 | except libcloud.common.google.ResourceExistsError: 142 | raise Exception( 143 | "tried creating a health check that already exists; " 144 | "please run 'deploy --check' to fix this" 145 | ) 146 | self.state = self.UP 147 | self.copy_properties(defn) 148 | 149 | # update the health check resource if its definition and state are out of sync 150 | if self.properties_changed(defn): 151 | self.log("updating properties of {0}...".format(self.full_name)) 152 | try: 153 | hc = self.healthcheck() 154 | hc.path = defn.path 155 | hc.port = defn.port 156 | hc.interval = defn.check_interval 157 | hc.timeout = defn.timeout 158 | hc.unhealthy_threshold = defn.unhealthy_threshold 159 | hc.healthy_threshold = defn.healthy_threshold 160 | hc.extra["host"] = defn.host 161 | hc.extra["description"] = defn.description 162 | hc.update() 163 | self.copy_properties(defn) 164 | except libcloud.common.google.ResourceNotFoundError: 165 | raise Exception( 166 | "{0} has been deleted behind our back; " 167 | "please run 'deploy --check' to fix this".format(self.full_name) 168 | ) 169 | 170 | def destroy(self, wipe=False): 171 | if self.state == self.UP: 172 | try: 173 | healthcheck = self.healthcheck() 174 | return self.confirm_destroy(healthcheck, self.full_name, abort=False) 175 | except libcloud.common.google.ResourceNotFoundError: 176 | self.warn( 177 | "tried to destroy {0} which didn't exist".format(self.full_name) 178 | ) 179 | return True 180 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Images. 4 | 5 | import os 6 | import libcloud.common.google 7 | 8 | from nixops.util import attr_property 9 | from nixops_gcp.gcp_common import ResourceDefinition, ResourceState 10 | from .types.gce_image import GceImageOptions 11 | 12 | 13 | class GCEImageDefinition(ResourceDefinition): 14 | """Definition of a GCE Image""" 15 | 16 | config: GceImageOptions 17 | 18 | @classmethod 19 | def get_type(cls): 20 | return "gce-image" 21 | 22 | @classmethod 23 | def get_resource_type(cls): 24 | return "gceImages" 25 | 26 | def __init__(self, name, config): 27 | super().__init__(name, config) 28 | self.image_name = self.config.name 29 | self.source_uri = self.config.sourceUri 30 | self.description = self.config.description 31 | 32 | def show_type(self): 33 | return self.get_type() 34 | 35 | 36 | class GCEImageState(ResourceState): 37 | """State of a GCE Image""" 38 | 39 | image_name = attr_property("gce.name", None) 40 | source_uri = attr_property("gce.sourceUri", None) 41 | description = attr_property("gce.description", None) 42 | 43 | @classmethod 44 | def get_type(cls): 45 | return "gce-image" 46 | 47 | def __init__(self, depl, name, id): 48 | ResourceState.__init__(self, depl, name, id) 49 | 50 | def show_type(self): 51 | return super(GCEImageState, self).show_type() 52 | 53 | @property 54 | def resource_id(self): 55 | return self.image_name 56 | 57 | nix_name = "gceImages" 58 | 59 | @property 60 | def full_name(self): 61 | return "GCE image '{0}'".format(self.image_name) 62 | 63 | def image(self): 64 | try: 65 | img = self.connect().ex_get_image(self.image_name) 66 | if img: 67 | img.destroy = img.delete 68 | return img 69 | except libcloud.common.google.ResourceNotFoundError: 70 | return None 71 | 72 | defn_properties = ["description", "source_uri"] 73 | 74 | def create(self, defn, check, allow_reboot, allow_recreate): 75 | if defn.name != "bootstrap": 76 | self.no_property_change(defn, "source_uri") 77 | self.no_property_change(defn, "description") 78 | 79 | self.no_project_change(defn) 80 | 81 | self.copy_credentials(defn) 82 | self.image_name = defn.image_name 83 | 84 | if check: 85 | image = self.image() 86 | if image: 87 | if self.state == self.UP: 88 | self.handle_changed_property( 89 | "description", image.extra["description"], can_fix=False 90 | ) 91 | else: 92 | self.warn_not_supposed_to_exist(valuable_data=True) 93 | self.confirm_destroy(image, self.full_name) 94 | else: 95 | self.warn_missing_resource() 96 | 97 | if self.state != self.UP: 98 | self.log("creating {0}...".format(self.full_name)) 99 | try: 100 | image = self.connect().ex_copy_image( 101 | defn.image_name, defn.source_uri, description=defn.description 102 | ) 103 | except libcloud.common.google.ResourceExistsError: 104 | raise Exception( 105 | "tried creating an image that already exists; " 106 | "please run 'deploy --check' to fix this" 107 | ) 108 | self.state = self.UP 109 | self.copy_properties(defn) 110 | 111 | def destroy(self, wipe=False): 112 | if self.state == self.UP: 113 | image = self.image() 114 | if image: 115 | return self.confirm_destroy(image, self.full_name, abort=False) 116 | else: 117 | self.warn( 118 | "tried to destroy {0} which didn't exist".format(self.full_name) 119 | ) 120 | return True 121 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Networks. 4 | 5 | import os 6 | import libcloud.common.google 7 | from libcloud.compute.types import Provider 8 | from libcloud.compute.providers import get_driver 9 | 10 | from nixops.util import attr_property 11 | from nixops_gcp.gcp_common import ResourceDefinition, ResourceState 12 | from .types.gce_network import GceNetworkOptions, FirewallOptions 13 | 14 | 15 | def normalize_list(tags): 16 | return sorted(tags or []) 17 | 18 | 19 | class GCENetworkDefinition(ResourceDefinition): 20 | """Definition of a GCE Network""" 21 | 22 | config: GceNetworkOptions 23 | 24 | @classmethod 25 | def get_type(cls): 26 | return "gce-network" 27 | 28 | @classmethod 29 | def get_resource_type(cls): 30 | return "gceNetworks" 31 | 32 | def __init__(self, name, config): 33 | super().__init__(name, config) 34 | 35 | self.network_name = self.config.name 36 | 37 | def parse_allowed(x): 38 | if x is None: 39 | return [] 40 | else: 41 | return [str(v) for v in x] 42 | 43 | def parse_fw(x_name: str, x: FirewallOptions): 44 | result = dict(x) 45 | result["sourceRanges"] = list(x.sourceRanges or []) or ["0.0.0.0/0"] 46 | result["allowed"] = { 47 | name: parse_allowed(a) for name, a in x.allowed.items() 48 | } 49 | 50 | if len(result["allowed"]) == 0: 51 | raise Exception( 52 | "Firewall rule '{0}' in network '{1}' " 53 | "must provide at least one protocol/port specification".format( 54 | x_name, self.network_name 55 | ) 56 | ) 57 | if len(result["sourceRanges"]) == 0 and len(result["sourceTags"]) == 0: 58 | raise Exception( 59 | "Firewall rule '{0}' in network '{1}' " 60 | "must specify at least one source range or tag".format( 61 | x_name, self.network_name 62 | ) 63 | ) 64 | return result 65 | 66 | self.firewall = {k: parse_fw(k, v) for k, v in self.config.firewall.items()} 67 | 68 | def show_type(self): 69 | return "{0}".format(self.get_type()) 70 | 71 | 72 | class GCENetworkState(ResourceState): 73 | """State of a GCE Network""" 74 | 75 | network_name = attr_property("gce.network_name", None) 76 | 77 | firewall = attr_property("gce.firewall", {}, "json") 78 | 79 | @classmethod 80 | def get_type(cls): 81 | return "gce-network" 82 | 83 | def __init__(self, depl, name, id): 84 | ResourceState.__init__(self, depl, name, id) 85 | 86 | def show_type(self): 87 | s = super(GCENetworkState, self).show_type() 88 | if self.state == self.UP: 89 | s = "{0}".format(s) 90 | return s 91 | 92 | @property 93 | def resource_id(self): 94 | return self.network_name 95 | 96 | nix_name = "gceNetworks" 97 | 98 | @property 99 | def full_name(self): 100 | return "GCE network '{0}'".format(self.network_name) 101 | 102 | def network(self): 103 | return self.connect().ex_get_network(self.network_name) 104 | 105 | def update_firewall(self, k, v): 106 | x = self.firewall 107 | if v == None: 108 | x.pop(k, None) 109 | else: 110 | x[k] = v 111 | self.firewall = x 112 | 113 | def firewall_name(self, name): 114 | return "{0}-{1}".format(self.network_name, name) 115 | 116 | def full_firewall_name(self, name): 117 | return "GCE firewall '{0}'".format(self.firewall_name(name)) 118 | 119 | def warn_if_firewall_changed( 120 | self, fw_name, expected_state, actual_state, name, can_fix=True 121 | ): 122 | return self.warn_if_changed( 123 | expected_state, 124 | actual_state, 125 | name, 126 | resource_name=self.full_firewall_name(fw_name), 127 | can_fix=can_fix, 128 | ) 129 | 130 | def destroy_firewall(self, fwname): 131 | self.log("destroying {0}...".format(self.full_firewall_name(fwname))) 132 | try: 133 | fw_n = self.firewall_name(fwname) 134 | self.connect().ex_get_firewall(fw_n).destroy() 135 | except libcloud.common.google.ResourceNotFoundError: 136 | self.warn( 137 | "tried to destroy {0} which didn't exist".format( 138 | self.full_firewall_name(fwname) 139 | ) 140 | ) 141 | self.update_firewall(fwname, None) 142 | 143 | def create(self, defn, check, allow_reboot, allow_recreate): 144 | self.no_project_change(defn) 145 | 146 | self.copy_credentials(defn) 147 | self.network_name = defn.network_name 148 | 149 | if check: 150 | try: 151 | network = self.network() 152 | if self.state != self.UP: 153 | self.warn_not_supposed_to_exist() 154 | self.confirm_destroy(network, self.full_name) 155 | 156 | except libcloud.common.google.ResourceNotFoundError: 157 | self.warn_missing_resource() 158 | 159 | if self.state != self.UP: 160 | self.log("creating {0}...".format(self.full_name)) 161 | try: 162 | network = self.connect().ex_create_network( 163 | defn.network_name, None, mode="auto" 164 | ) 165 | except libcloud.common.google.ResourceExistsError: 166 | raise Exception( 167 | "tried creating a network that already exists; " 168 | "please run 'deploy --check' to fix this" 169 | ) 170 | self.state = self.UP 171 | 172 | # handle firewall rules 173 | def trans_allowed(attrs): 174 | return [ 175 | dict( 176 | [("IPProtocol", proto)] 177 | + ([("ports", ports)] if ports is not None else []) 178 | ) 179 | for proto, ports in attrs.items() 180 | ] 181 | 182 | if check: 183 | firewalls = [ 184 | f 185 | for f in self.connect().ex_list_firewalls() 186 | if f.network.name == defn.network_name 187 | ] 188 | 189 | # delete stray rules and mark changed ones for update 190 | for fw in firewalls: 191 | fw_name = next( 192 | ( 193 | k 194 | for (k, v) in self.firewall.items() 195 | if fw.name == self.firewall_name(k) 196 | ), 197 | None, 198 | ) 199 | if fw_name: 200 | rule = self.firewall[fw_name] 201 | 202 | rule["sourceRanges"] = self.warn_if_firewall_changed( 203 | fw_name, 204 | rule["sourceRanges"], 205 | normalize_list(fw.source_ranges), 206 | "source ranges", 207 | ) 208 | rule["sourceTags"] = self.warn_if_firewall_changed( 209 | fw_name, 210 | rule["sourceTags"], 211 | normalize_list(fw.source_tags), 212 | "source tags", 213 | ) 214 | rule["targetTags"] = self.warn_if_firewall_changed( 215 | fw_name, 216 | rule["targetTags"], 217 | normalize_list(fw.target_tags), 218 | "target tags", 219 | ) 220 | 221 | if fw.allowed != trans_allowed(rule["allowed"]): 222 | self.warn( 223 | "{0} allowed ports and protocols have changed unexpectedly".format( 224 | self.full_firewall_name(fw_name) 225 | ) 226 | ) 227 | rule["allowed"] = {} # mark for update 228 | 229 | self.update_firewall(fw_name, rule) 230 | else: 231 | self.warn( 232 | "deleting {0} which isn't supposed to exist...".format( 233 | self.firewall_name(fw_name) 234 | ) 235 | ) 236 | fw.destroy() 237 | 238 | # find missing firewall rules 239 | for k, v in self.firewall.items(): 240 | if not any(fw.name == self.firewall_name(k) for fw in firewalls): 241 | self.warn("firewall rule '{0}' has disappeared...".format(k)) 242 | self.update_firewall(k, None) 243 | 244 | # add new and update changed 245 | for k, v in defn.firewall.items(): 246 | if k in self.firewall: 247 | if v == self.firewall[k]: 248 | continue 249 | self.log("updating {0}...".format(self.firewall_name(k))) 250 | try: 251 | firewall = self.connect().ex_get_firewall(self.firewall_name(k)) 252 | firewall.allowed = trans_allowed(v["allowed"]) 253 | firewall.source_ranges = v["sourceRanges"] 254 | firewall.source_tags = v["sourceTags"] 255 | firewall.target_tags = v["targetTags"] 256 | firewall.update() 257 | except libcloud.common.google.ResourceNotFoundError: 258 | raise Exception( 259 | "tried updating a firewall rule that doesn't exist; " 260 | "please run 'deploy --check' to fix this" 261 | ) 262 | 263 | else: 264 | self.log("creating {0}...".format(self.full_firewall_name(k))) 265 | try: 266 | self.connect().ex_create_firewall( 267 | self.firewall_name(k), 268 | trans_allowed(v["allowed"]), 269 | network=self.network_name, 270 | source_ranges=v["sourceRanges"], 271 | source_tags=v["sourceTags"], 272 | target_tags=v["targetTags"], 273 | ) 274 | except libcloud.common.google.ResourceExistsError: 275 | raise Exception( 276 | "tried creating a firewall rule that already exists; " 277 | "please run 'deploy --check' to fix this" 278 | ) 279 | 280 | self.update_firewall(k, v) 281 | 282 | # delete unneeded 283 | for k in set(self.firewall.keys()) - set(defn.firewall.keys()): 284 | self.destroy_firewall(k) 285 | 286 | def destroy(self, wipe=False): 287 | if self.state == self.UP: 288 | try: 289 | network = self.network() 290 | if not self.depl.logger.confirm( 291 | "are you sure you want to destroy {0}?".format(self.full_name) 292 | ): 293 | return False 294 | 295 | for k in self.firewall.keys(): 296 | self.destroy_firewall(k) 297 | 298 | self.log("destroying {0}...".format(self.full_name)) 299 | network.destroy() 300 | except libcloud.common.google.ResourceNotFoundError: 301 | self.warn( 302 | "tried to destroy {0} which didn't exist".format(self.full_name) 303 | ) 304 | 305 | return True 306 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_route.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Routes. 4 | 5 | import libcloud.common.google 6 | 7 | from nixops import backends 8 | from nixops.util import attr_property 9 | from nixops_gcp.gcp_common import ResourceDefinition, ResourceState 10 | from .types.gce_route import GceRouteOptions 11 | 12 | 13 | class GCERouteDefinition(ResourceDefinition): 14 | """Definition of a GCE Route""" 15 | 16 | config: GceRouteOptions 17 | 18 | @classmethod 19 | def get_type(cls): 20 | return "gce-route" 21 | 22 | @classmethod 23 | def get_resource_type(cls): 24 | return "gceRoutes" 25 | 26 | def __init__(self, name, config): 27 | super().__init__(name, config) 28 | self.route_name = self.config.name 29 | self.description = self.config.description 30 | self.network = self.config.network 31 | self.priority = self.config.priority 32 | self.nextHop = self.config.nextHop 33 | self.destination = self.config.destination 34 | self.tags = list(tags) if self.tags is not None else None 35 | 36 | def show_type(self): 37 | return "{0} [{1}]".format(self.get_type(), self.name) 38 | 39 | 40 | class GCERouteState(ResourceState): 41 | """State of a GCE Route""" 42 | 43 | route_name = attr_property("gce.route.name", None) 44 | description = attr_property("gce.route.description", None) 45 | network = attr_property("gce.route.network", None) 46 | priority = attr_property("gce.route.priority", None, int) 47 | nextHop = attr_property("gce.route.nextHop", None) 48 | destination = attr_property("gce.route.destination", None) 49 | tags = attr_property("gce.route.tags", None, "json") 50 | 51 | defn_properties = [ 52 | "route_name", 53 | "destination", 54 | "priority", 55 | "network", 56 | "tags", 57 | "nextHop", 58 | "description", 59 | ] 60 | 61 | nix_name = "gceRoutes" 62 | 63 | @classmethod 64 | def get_type(cls): 65 | return "gce-route" 66 | 67 | def __init__(self, depl, name, id): 68 | ResourceState.__init__(self, depl, name, id) 69 | 70 | def show_type(self): 71 | s = super(GCERouteState, self).show_type() 72 | if self.state == self.UP: 73 | s = "{0} [{1}]".format(s, self.name) 74 | return s 75 | 76 | def _destroy_route(self): 77 | try: 78 | route = self.connect().ex_get_route(self.route_name) 79 | route.destroy() 80 | except libcloud.common.google.ResourceNotFoundError: 81 | self.warn("tried to destroy {0} which didn't exist".format(self.full_name)) 82 | 83 | def create_after(self, resources, defn): 84 | return {r for r in resources if isinstance(r, backends.MachineState)} 85 | 86 | @property 87 | def full_name(self): 88 | return "GCE route '{0}'".format(self.name) 89 | 90 | def _route_is_missing(self): 91 | try: 92 | self.connect().ex_get_route(self.route_name) 93 | return False 94 | except libcloud.common.google.ResourceNotFoundError: 95 | return True 96 | 97 | def _real_state_differ(self): 98 | """ Check If any of the route's properties has a different value than that in the state""" 99 | route = self.connect().ex_get_route(self.route_name) 100 | # libcloud only expose these properties in the GCERoute class. 101 | # "description" and "nextHop" can't be checked. 102 | route_properties = { 103 | "name": "route_name", 104 | "dest_range": "destination", 105 | "tags": "tags", 106 | "priority": "priority", 107 | } 108 | # This shouldn't happen, unless you delete the 109 | # route manually and create another one with the 110 | # same name, but different properties. 111 | real_state_differ = any( 112 | [ 113 | getattr(route, route_attr) != getattr(self, self_attr) 114 | for route_attr, self_attr in route_properties.items() 115 | ] 116 | ) 117 | 118 | # We need to check the network in separate, since GCE API add the project and the region 119 | network_differ = route.network.split("/")[-1] != self.network 120 | 121 | return real_state_differ or network_differ 122 | 123 | def _get_machine_property(self, machine_name, property): 124 | """Get a property from the machine """ 125 | machine = self.depl.get_machine(machine_name) 126 | return getattr(machine, property) 127 | 128 | def _check(self): 129 | if self._route_is_missing(): 130 | self.state = self.MISSING 131 | return False 132 | 133 | if self._real_state_differ(): 134 | if self.depl.logger.confirm( 135 | "Route properties are different from those in the state, " 136 | "destroy route {0}?".format(self.route_name) 137 | ): 138 | self._destroy_route() 139 | self.state = self.MISSING 140 | return True 141 | 142 | def create(self, defn, check, allow_reboot, allow_recreate): 143 | self.copy_credentials(defn) 144 | 145 | if check: 146 | if self._route_is_missing(): 147 | self.state = self.MISSING 148 | 149 | elif self._real_state_differ(): 150 | if allow_recreate: 151 | self._destroy_route() 152 | self.state = self.MISSING 153 | else: 154 | self.warn( 155 | "Route properties are different from those in the state," 156 | " use --allow-recreate to delete the route and deploy it again." 157 | ) 158 | 159 | if defn.destination.startswith("res-"): 160 | # if a machine resource was used for the destination, get 161 | # the public IP of the instance into the definition of the 162 | # route 163 | machine_name = defn.destination[4:] 164 | defn.destination = "{ip}/32".format( 165 | ip=self._get_machine_property(machine_name, "public_ipv4") 166 | ) 167 | 168 | if self.is_deployed() and self.properties_changed(defn): 169 | if allow_recreate: 170 | self.log("deleting route {0}...".format(self.route_name)) 171 | self._destroy_route() 172 | self.state = self.MISSING 173 | else: 174 | raise Exception( 175 | "GCE routes are immutable, you need to use --allow-recreate." 176 | ) 177 | 178 | if self.state != self.UP: 179 | with self.depl._db: 180 | self.log("creating {0}...".format(self.full_name)) 181 | self.copy_properties(defn) 182 | 183 | if defn.nextHop and defn.nextHop.startswith("res-"): 184 | try: 185 | nextHop_name = self._get_machine_property( 186 | defn.nextHop[4:], "machine_name" 187 | ) 188 | defn.nextHop = self.connect().ex_get_node(nextHop_name) 189 | except AttributeError: 190 | raise Exception("nextHop can only be a GCE machine.") 191 | raise 192 | except libcloud.common.google.ResourceNotFoundError: 193 | raise Exception( 194 | "The machine {0} isn't deployed, it need to be before it's added as nextHop".format( 195 | nextHop_name 196 | ) 197 | ) 198 | 199 | args = [getattr(defn, attr) for attr in self.defn_properties] 200 | try: 201 | self.connect().ex_create_route(*args) 202 | except libcloud.common.google.ResourceExistsError: 203 | raise Exception("tried creating a route that already exists.") 204 | self.state = self.UP 205 | 206 | def destroy(self, wipe=False): 207 | if self.state == self.UP: 208 | if not self.depl.logger.confirm( 209 | "are you sure you want to destroy {0}?".format(self.full_name) 210 | ): 211 | return False 212 | 213 | self.log("destroying {0}...".format(self.full_name)) 214 | self._destroy_route() 215 | 216 | return True 217 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_static_ip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Static/Reserved IP addresses. 4 | 5 | import os 6 | import libcloud.common.google 7 | 8 | from nixops.util import attr_property 9 | from nixops_gcp.gcp_common import ResourceDefinition, ResourceState, optional_string 10 | from .types.gce_static_ip import GceStaticIpOptions 11 | 12 | 13 | class GCEStaticIPDefinition(ResourceDefinition): 14 | """Definition of a GCE Static IP""" 15 | 16 | config: GceStaticIpOptions 17 | 18 | @classmethod 19 | def get_type(cls): 20 | return "gce-static-ip" 21 | 22 | @classmethod 23 | def get_resource_type(cls): 24 | return "gceStaticIPs" 25 | 26 | def __init__(self, name, config): 27 | super().__init__(name, config) 28 | self.addr_name = self.config.name 29 | self.region = self.config.region 30 | self.ip_address = self.config.ipAddress 31 | 32 | def show_type(self): 33 | return "{0} [{1}]".format(self.get_type(), self.region) 34 | 35 | 36 | class GCEStaticIPState(ResourceState): 37 | """State of a GCE Static IP""" 38 | 39 | region = attr_property("gce.region", None) 40 | addr_name = attr_property("gce.name", None) 41 | ip_address = attr_property("gce.ipAddress", None) 42 | 43 | @classmethod 44 | def get_type(cls): 45 | return "gce-static-ip" 46 | 47 | def __init__(self, depl, name, id): 48 | ResourceState.__init__(self, depl, name, id) 49 | 50 | def show_type(self): 51 | s = super(GCEStaticIPState, self).show_type() 52 | if self.state == self.UP: 53 | s = "{0} [{1}]".format(s, self.region) 54 | return s 55 | 56 | @property 57 | def resource_id(self): 58 | return self.addr_name 59 | 60 | nix_name = "gceStaticIPs" 61 | 62 | @property 63 | def full_name(self): 64 | return "GCE static IP address '{0}'".format(self.addr_name) 65 | 66 | def address(self): 67 | return self.connect().ex_get_address(self.addr_name, region=self.region) 68 | 69 | @property 70 | def public_ipv4(self): 71 | return self.ip_address 72 | 73 | def prefix_definition(self, attr): 74 | return {("resources", "gceStaticIPs"): attr} 75 | 76 | def get_physical_spec(self): 77 | return {"publicIPv4": self.public_ipv4} 78 | 79 | def create(self, defn, check, allow_reboot, allow_recreate): 80 | self.no_change( 81 | defn.ip_address and self.ip_address != defn.ip_address, "address" 82 | ) 83 | self.no_project_change(defn) 84 | self.no_region_change(defn) 85 | 86 | self.copy_credentials(defn) 87 | self.addr_name = defn.addr_name 88 | 89 | if check: 90 | try: 91 | address = self.address() 92 | if self.state == self.UP: 93 | self.handle_changed_property( 94 | "ip_address", address.address, property_name="" 95 | ) 96 | self.handle_changed_property( 97 | "region", address.region.name, can_fix=False 98 | ) 99 | else: 100 | self.warn_not_supposed_to_exist(valuable_resource=True) 101 | self.confirm_destroy(address, self.full_name) 102 | 103 | except libcloud.common.google.ResourceNotFoundError: 104 | self.warn_missing_resource() 105 | 106 | if self.state != self.UP: 107 | self.log("reserving {0} in {1}...".format(self.full_name, defn.region)) 108 | try: 109 | address = self.connect().ex_create_address( 110 | defn.addr_name, region=defn.region, address=defn.ip_address 111 | ) 112 | except libcloud.common.google.ResourceExistsError: 113 | raise Exception( 114 | "tried requesting a static IP that already exists; " 115 | "please run 'deploy --check' to fix this" 116 | ) 117 | 118 | self.log("reserved IP address: {0}".format(address.address)) 119 | self.state = self.UP 120 | self.region = defn.region 121 | self.ip_address = address.address 122 | 123 | def destroy(self, wipe=False): 124 | if self.state == self.UP: 125 | try: 126 | address = self.address() 127 | return self.confirm_destroy( 128 | address, 129 | "{0} ({1})".format(self.full_name, self.ip_address), 130 | abort=False, 131 | ) 132 | except libcloud.common.google.ResourceNotFoundError: 133 | self.warn( 134 | "tried to destroy {0} which didn't exist".format(self.full_name) 135 | ) 136 | return True 137 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gce_target_pool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GCE Target Pools 4 | 5 | import os 6 | import libcloud.common.google 7 | 8 | from nixops_gcp.resources.gce_http_health_check import GCEHTTPHealthCheckState 9 | from nixops.util import attr_property 10 | from nixops_gcp.gcp_common import ResourceDefinition, ResourceState, optional_string 11 | from .types.gce_target_pool import GceTargetPoolOptions 12 | 13 | 14 | class GCETargetPoolDefinition(ResourceDefinition): 15 | """Definition of a GCE Target Pool""" 16 | 17 | config: GceTargetPoolOptions 18 | 19 | @classmethod 20 | def get_type(cls): 21 | return "gce-target-pool" 22 | 23 | @classmethod 24 | def get_resource_type(cls): 25 | return "gceTargetPools" 26 | 27 | def __init__(self, name, config): 28 | super().__init__(name, config) 29 | 30 | self.targetpool_name = self.config.name 31 | self.region = self.config.region 32 | self.health_check = self.config.healthCheck 33 | 34 | # Previously this option accepted mixed types 35 | # def machine_to_url(xml): 36 | # spec = xml.find("attr[@name='gce']") 37 | # if spec is None: 38 | # return None 39 | # return "https://www.googleapis.com/compute/v1/projects/{0}/zones/{1}/instances/{2}".format( 40 | # self.project, 41 | # spec.find("attrs/attr[@name='region']/string").get("value"), 42 | # spec.find("attrs/attr[@name='machineName']/string").get("value"), 43 | # ) 44 | 45 | # Previously mixed types was accepted 46 | self.machines = list(self.config.machines) 47 | 48 | if not all(m for m in self.machines): 49 | raise Exception( 50 | "target pool machine specification must be either a NixOps " 51 | "machine resource or a fully-qualified GCE resource URL" 52 | ) 53 | 54 | # FIXME: implement backup pool, failover ratio, description, sessionAffinity 55 | 56 | def show_type(self): 57 | return "{0} [{1}]".format(self.get_type(), self.region) 58 | 59 | 60 | class GCETargetPoolState(ResourceState): 61 | """State of a GCE Target Pool""" 62 | 63 | targetpool_name = attr_property("gce.name", None) 64 | region = attr_property("gce.region", None) 65 | health_check = attr_property("gce.healthcheck", None) 66 | machines = attr_property("gce.machines", [], "json") 67 | 68 | @classmethod 69 | def get_type(cls): 70 | return "gce-target-pool" 71 | 72 | def __init__(self, depl, name, id): 73 | ResourceState.__init__(self, depl, name, id) 74 | 75 | def show_type(self): 76 | s = super(GCETargetPoolState, self).show_type() 77 | if self.state == self.UP: 78 | s = "{0} [{1}]".format(s, self.region) 79 | return s 80 | 81 | @property 82 | def resource_id(self): 83 | return self.targetpool_name 84 | 85 | nix_name = "gceTargetPools" 86 | 87 | @property 88 | def full_name(self): 89 | return "GCE target pool '{0}'".format(self.targetpool_name) 90 | 91 | def targetpool(self): 92 | return self.connect().ex_get_targetpool(self.targetpool_name) 93 | 94 | defn_properties = ["region", "health_check"] 95 | 96 | def create(self, defn, check, allow_reboot, allow_recreate): 97 | self.no_project_change(defn) 98 | self.no_region_change(defn) 99 | 100 | self.copy_credentials(defn) 101 | self.targetpool_name = defn.targetpool_name 102 | 103 | if check: 104 | try: 105 | tp = self.targetpool() 106 | if self.state == self.UP: 107 | 108 | self.handle_changed_property( 109 | "region", tp.region.name, can_fix=False 110 | ) 111 | 112 | normalized_hc = tp.healthchecks[0].name if tp.healthchecks else None 113 | self.handle_changed_property("health_check", normalized_hc) 114 | 115 | normalized_machines = set( 116 | [ 117 | n.extra["selfLink"] if hasattr(n, "extra") else n 118 | for n in tp.nodes 119 | ] 120 | ) 121 | machines_state = set(self.machines) 122 | if machines_state != normalized_machines: 123 | if normalized_machines - machines_state: 124 | self.warn( 125 | "{0} contains unexpected machines: {1}".format( 126 | self.full_name, 127 | list(normalized_machines - machines_state), 128 | ) 129 | ) 130 | if machines_state - normalized_machines: 131 | self.warn( 132 | "{0} is missing machines: {1}".format( 133 | self.full_name, 134 | list(machines_state - normalized_machines), 135 | ) 136 | ) 137 | self.machines = list(normalized_machines) 138 | 139 | else: 140 | self.warn_not_supposed_to_exist() 141 | self.confirm_destroy(tp, self.full_name) 142 | 143 | except libcloud.common.google.ResourceNotFoundError: 144 | self.warn_missing_resource() 145 | 146 | if self.state != self.UP: 147 | self.log("creating {0}...".format(self.full_name)) 148 | try: 149 | tp = self.connect().ex_create_targetpool( 150 | defn.targetpool_name, 151 | region=defn.region, 152 | healthchecks=([defn.health_check] if defn.health_check else None), 153 | ) 154 | except libcloud.common.google.ResourceExistsError: 155 | raise Exception( 156 | "tried creating a target pool that already exists; " 157 | "please run 'deploy --check' to fix this" 158 | ) 159 | self.state = self.UP 160 | self.copy_properties(defn) 161 | self.machines = [] 162 | 163 | # update the target pool resource if its definition and state are out of sync 164 | machines_state = set(self.machines) 165 | machines_defn = set(defn.machines) 166 | if self.health_check != defn.health_check or machines_state != machines_defn: 167 | try: 168 | tp = self.targetpool() 169 | except libcloud.common.google.ResourceNotFoundError: 170 | raise Exception( 171 | "{0} has been deleted behind our back; " 172 | "please run 'deploy --check' to fix this".format(self.full_name) 173 | ) 174 | 175 | if self.health_check != defn.health_check: 176 | self.log("ppdating the health check of {0}...".format(self.full_name)) 177 | if self.health_check: 178 | tp.remove_healthcheck(self.health_check) 179 | self.health_check = None 180 | if defn.health_check: 181 | tp.add_healthcheck(defn.health_check) 182 | self.health_check = defn.health_check 183 | 184 | if machines_state != machines_defn: 185 | self.log("updating the machine list of {0}...".format(self.full_name)) 186 | for uri in machines_state - machines_defn: 187 | tp.remove_node(uri) 188 | machines_state.remove(uri) 189 | for uri in machines_defn - machines_state: 190 | tp.add_node(uri) 191 | machines_state.add(uri) 192 | self.machines = list(machines_state) 193 | 194 | def destroy(self, wipe=False): 195 | if self.state == self.UP: 196 | try: 197 | targetpool = self.targetpool() 198 | return self.confirm_destroy(targetpool, self.full_name, abort=False) 199 | except libcloud.common.google.ResourceNotFoundError: 200 | self.warn( 201 | "tried to destroy {0} which didn't exist".format(self.full_name) 202 | ) 203 | return True 204 | 205 | def create_after(self, resources, defn): 206 | return {r for r in resources if isinstance(r, GCEHTTPHealthCheckState)} 207 | -------------------------------------------------------------------------------- /nixops_gcp/resources/gse_bucket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Automatic provisioning of GSE Buckets 4 | 5 | import os 6 | import re 7 | import libcloud.common.google 8 | 9 | from nixops.util import attr_property 10 | from nixops_gcp.gcp_common import ( 11 | ResourceDefinition, 12 | ResourceState, 13 | optional_string, 14 | optional_int, 15 | optional_bool, 16 | ) 17 | 18 | from typing import Dict, Optional 19 | from .types.gse_bucket import GseBucketOptions, LifecycleOptions, ConditionsOptions 20 | 21 | 22 | class GSEResponse(libcloud.common.google.GoogleResponse): 23 | pass 24 | 25 | 26 | class GSEConnection(libcloud.common.google.GoogleBaseConnection): 27 | """Connection class for the GSE""" 28 | 29 | host = "www.googleapis.com" 30 | responseCls = GSEResponse 31 | 32 | def __init__(self, user_id, key, secure, **kwargs): 33 | self.scope = ["https://www.googleapis.com/auth/devstorage.read_write"] 34 | super(GSEConnection, self).__init__(user_id, key, secure=secure, **kwargs) 35 | self.request_path = "/storage/v1/b" 36 | 37 | def _get_token_info_from_file(self): 38 | return None 39 | 40 | def _write_token_info_to_file(self): 41 | return 42 | 43 | 44 | class GSEBucketDefinition(ResourceDefinition): 45 | """Definition of a GSE Bucket""" 46 | 47 | config: GseBucketOptions 48 | 49 | @classmethod 50 | def get_type(cls): 51 | return "gse-bucket" 52 | 53 | @classmethod 54 | def get_resource_type(cls): 55 | return "gseBuckets" 56 | 57 | def __init__(self, name, config): 58 | super().__init__(name, config) 59 | 60 | self.bucket_name = self.config.name 61 | 62 | self.cors = dict(self.config.cors) 63 | self.cors["methods"] = list(self.config.cors.methods) 64 | self.cors["origins"] = list(self.config.cors.origins) 65 | 66 | def parse_lifecycle(x: LifecycleOptions) -> Dict: 67 | created_before = x.conditions.createdBefore 68 | normalized_created_before: Optional[str] = None 69 | if created_before: 70 | m = re.match(r"^(\d*)-(\d*)-(\d*)$", created_before) 71 | if m: 72 | normalized_created_before = "{0[0]:0>4}-{0[1]:0>2}-{0[2]:0>2}".format( 73 | m.groups() 74 | ) 75 | else: 76 | raise Exception( 77 | "createdBefore must be a date in 'YYYY-MM-DD' format" 78 | ) 79 | return { 80 | "action": x.action, 81 | "age": x.conditions.age, 82 | "is_live": x.conditions.isLive, 83 | "created_before": normalized_created_before, 84 | "number_of_newer_versions": x.conditions.numberOfNewerVersions, 85 | } 86 | 87 | self.lifecycle = [parse_lifecycle(x) for x in self.config.lifecycle] 88 | 89 | if any( 90 | all(v is None for k, v in r.items() if k != "action") 91 | for r in self.lifecycle 92 | ): 93 | raise Exception( 94 | "Bucket '{0}' object lifecycle management " 95 | "rule must specify at least one condition".format(self.bucket_name) 96 | ) 97 | 98 | self.log_bucket = self.config.logging.logBucket 99 | self.log_object_prefix = self.config.logging.logObjectPrefix 100 | 101 | self.region = self.config.location 102 | self.storage_class = self.config.storageClass 103 | self.versioning_enabled = self.config.versioning.enabled 104 | 105 | self.website_main_page_suffix = self.config.website.mainPageSuffix 106 | self.website_not_found_page = self.config.website.notFoundPage 107 | 108 | def show_type(self): 109 | return "{0}".format(self.get_type()) 110 | 111 | 112 | class GSEBucketState(ResourceState): 113 | """State of a GSE Bucket""" 114 | 115 | bucket_name = attr_property("gce.name", None) 116 | 117 | cors = attr_property("gce.cors", [], "json") 118 | lifecycle = attr_property("gce.lifecycle", [], "json") 119 | log_bucket = attr_property("gce.logBucket", None) 120 | log_object_prefix = attr_property("gce.logObjectPrefix", None) 121 | region = attr_property("gce.region", None) 122 | storage_class = attr_property("gce.storageClass", None) 123 | versioning_enabled = attr_property("gce.versioningEnabled", None, bool) 124 | website_main_page_suffix = attr_property("gce.websiteMainPageSuffix", None) 125 | website_not_found_page = attr_property("gce.websiteNotFoundPage", None) 126 | 127 | @classmethod 128 | def get_type(cls): 129 | return "gse-bucket" 130 | 131 | def __init__(self, depl, name, id): 132 | ResourceState.__init__(self, depl, name, id) 133 | 134 | def show_type(self): 135 | s = super(GSEBucketState, self).show_type() 136 | if self.state == self.UP: 137 | s = "{0}".format(s) 138 | return s 139 | 140 | @property 141 | def resource_id(self): 142 | return self.bucket_name 143 | 144 | nix_name = "gseBuckets" 145 | 146 | @property 147 | def full_name(self): 148 | return "GSE bucket '{0}'".format(self.bucket_name) 149 | 150 | def connect(self): 151 | if not self._conn: 152 | self._conn = GSEConnection(self.service_account, self.access_key_path, True) 153 | return self._conn 154 | 155 | defn_properties = [ 156 | "cors", 157 | "lifecycle", 158 | "log_bucket", 159 | "log_object_prefix", 160 | "region", 161 | "storage_class", 162 | "versioning_enabled", 163 | "website_main_page_suffix", 164 | "website_not_found_page", 165 | ] 166 | 167 | def bucket_resource(self, defn): 168 | return { 169 | "name": defn.bucket_name, 170 | "cors": [ 171 | { 172 | "origin": c["origins"], 173 | "method": c["methods"], 174 | "responseHeader": c["response_headers"], 175 | "maxAgeSeconds": c["max_age_seconds"], 176 | } 177 | for c in defn.cors 178 | ], 179 | "lifecycle": { 180 | "rule": [ 181 | { 182 | "action": {"type": r["action"]}, 183 | "condition": { 184 | "age": r["age"], 185 | "isLive": r["is_live"], 186 | "createdBefore": r["created_before"], 187 | "numNewerVersions": r["number_of_newer_versions"], 188 | }, 189 | } 190 | for r in defn.lifecycle 191 | ] 192 | }, 193 | "location": defn.region, 194 | "logging": { 195 | "logBucket": defn.log_bucket, 196 | "logObjectPrefix": defn.log_object_prefix, 197 | } 198 | if defn.log_bucket is not None 199 | else {}, 200 | "storageClass": defn.storage_class, 201 | "versioning": {"enabled": defn.versioning_enabled}, 202 | "website": { 203 | "mainPageSuffix": defn.website_main_page_suffix, 204 | "notFoundPage": defn.website_not_found_page, 205 | }, 206 | } 207 | 208 | def bucket(self): 209 | return ( 210 | self.connect() 211 | .request("/{0}?projection=full".format(self.bucket_name), method="GET") 212 | .object 213 | ) 214 | 215 | def delete_bucket(self): 216 | return self.connect().request("/{0}".format(self.bucket_name), method="DELETE") 217 | 218 | def create_bucket(self, defn): 219 | return self.connect().request( 220 | "?project={0}".format(self.project), 221 | method="POST", 222 | data=self.bucket_resource(defn), 223 | ) 224 | 225 | def update_bucket(self, defn): 226 | return self.connect().request( 227 | "/{0}".format(self.bucket_name), 228 | method="PATCH", 229 | data=self.bucket_resource(defn), 230 | ) 231 | 232 | def create(self, defn, check, allow_reboot, allow_recreate): 233 | self.no_property_change(defn, "storage_class") 234 | self.no_project_change(defn) 235 | self.no_region_change(defn) 236 | 237 | self.copy_credentials(defn) 238 | self.bucket_name = defn.bucket_name 239 | 240 | if check: 241 | try: 242 | b = self.bucket() 243 | if self.state == self.UP: 244 | 245 | self.handle_changed_property("region", b["location"], can_fix=False) 246 | self.handle_changed_property( 247 | "storage_class", b["storageClass"], can_fix=False 248 | ) 249 | 250 | self.handle_changed_property( 251 | "log_bucket", b.get("logging", {}).get("logBucket", None) 252 | ) 253 | self.handle_changed_property( 254 | "log_object_prefix", 255 | b.get("logging", {}).get("logObjectPrefix", None), 256 | ) 257 | self.handle_changed_property( 258 | "versioning_enabled", b["versioning"]["enabled"] 259 | ) 260 | self.handle_changed_property( 261 | "website_main_page_suffix", 262 | b.get("website", {}).get("mainPageSuffix", None), 263 | ) 264 | self.handle_changed_property( 265 | "website_not_found_page", 266 | b.get("website", {}).get("notFoundPage", None), 267 | ) 268 | 269 | actual_cors = sorted( 270 | [ 271 | { 272 | "origins": sorted(c.get("origin", [])), 273 | "methods": sorted(c.get("method", [])), 274 | "response_headers": sorted(c.get("responseHeader", [])), 275 | "max_age_seconds": int(c.get("maxAgeSeconds")), 276 | } 277 | for c in b.get("cors", {}) 278 | ] 279 | ) 280 | self.handle_changed_property( 281 | "cors", actual_cors, property_name="CORS config" 282 | ) 283 | 284 | actual_lifecycle = sorted( 285 | [ 286 | { 287 | "action": r.get("action", {}).get("type", None), 288 | "age": r.get("condition", {}).get("age", None), 289 | "is_live": r.get("condition", {}).get("isLive", None), 290 | "created_before": r.get("condition", {}).get( 291 | "createdBefore", None 292 | ), 293 | "number_of_newer_versions": r.get("condition", {}).get( 294 | "numNewerVersions", None 295 | ), 296 | } 297 | for r in b.get("lifecycle", {}).get("rule", []) 298 | ] 299 | ) 300 | self.handle_changed_property( 301 | "lifecycle", actual_lifecycle, property_name="lifecycle config" 302 | ) 303 | 304 | else: 305 | self.warn_not_supposed_to_exist( 306 | valuable_resource=True, valuable_data=True 307 | ) 308 | if self.depl.logger.confirm( 309 | "are you sure you want to destroy the existing {0}?".format( 310 | self.full_name 311 | ) 312 | ): 313 | self.log("destroying...") 314 | self.delete_bucket() 315 | else: 316 | raise Exception("can't proceed further") 317 | 318 | except libcloud.common.google.ResourceNotFoundError: 319 | self.warn_missing_resource() 320 | 321 | if self.state != self.UP: 322 | self.log("creating {0}...".format(self.full_name)) 323 | try: 324 | bucket = self.create_bucket(defn) 325 | except libcloud.common.google.GoogleBaseError as e: 326 | if ( 327 | e.value.get("message", None) 328 | == "You already own this bucket. Please select another name." 329 | ): 330 | raise Exception( 331 | "tried creating a GSE bucket that already exists; " 332 | "please run 'deploy --check' to fix this" 333 | ) 334 | else: 335 | raise 336 | self.state = self.UP 337 | self.copy_properties(defn) 338 | 339 | if self.properties_changed(defn): 340 | self.log("updating {0}...".format(self.full_name)) 341 | self.update_bucket(defn) 342 | self.copy_properties(defn) 343 | 344 | def destroy(self, wipe=False): 345 | if self.state == self.UP: 346 | try: 347 | bucket = self.bucket() 348 | if not self.depl.logger.confirm( 349 | "are you sure you want to destroy {0}?".format(self.full_name) 350 | ): 351 | return False 352 | self.log("destroying {0}...".format(self.full_name)) 353 | self.delete_bucket() 354 | except libcloud.common.google.ResourceNotFoundError: 355 | self.warn( 356 | "tried to destroy {0} which didn't exist".format(self.full_name) 357 | ) 358 | return True 359 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixops-gce/d13cb794aef763338f544010ceb1816fe31d7f42/nixops_gcp/resources/types/__init__.py -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_disk.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from nixops.resources import ResourceOptions 3 | 4 | from nixops_gcp.backends.options import ImageOptions 5 | 6 | 7 | class GceDiskOptions(ResourceOptions): 8 | accessKey: str 9 | diskType: str 10 | image: ImageOptions 11 | name: str 12 | project: str 13 | region: str 14 | serviceAccount: str 15 | size: Optional[int] 16 | snapshot: Optional[str] 17 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_forwarding_rule.py: -------------------------------------------------------------------------------- 1 | from nixops.resources import ResourceOptions 2 | from typing import Union 3 | from typing import Optional 4 | 5 | 6 | class GceForwardingRuleOptions(ResourceOptions): 7 | accessKey: str 8 | description: Optional[str] 9 | ipAddress: Optional[str] 10 | name: str 11 | portRange: Optional[str] 12 | project: str 13 | protocol: str 14 | publicIPv4: Optional[str] 15 | region: str 16 | serviceAccount: str 17 | targetPool: str 18 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_http_health_check.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from nixops.resources import ResourceOptions 3 | from typing import Optional 4 | 5 | 6 | class GceHttpHealthCheckOptions(ResourceOptions): 7 | accessKey: str 8 | checkInterval: int 9 | description: Optional[str] 10 | healthyThreshold: int 11 | host: Optional[str] 12 | name: str 13 | path: str 14 | port: int 15 | project: str 16 | serviceAccount: str 17 | timeout: int 18 | unhealthyThreshold: int 19 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_image.py: -------------------------------------------------------------------------------- 1 | from nixops.resources import ResourceOptions 2 | from typing import Optional 3 | from typing import Union 4 | 5 | 6 | class GceImageOptions(ResourceOptions): 7 | accessKey: str 8 | description: Optional[str] 9 | name: str 10 | project: str 11 | serviceAccount: str 12 | sourceUri: str 13 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_network.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | from nixops.resources import ResourceOptions 3 | from typing import Mapping 4 | from typing import Optional 5 | from typing import Union 6 | 7 | 8 | class FirewallOptions(ResourceOptions): 9 | allowed: Mapping[str, Optional[Sequence[Union[int, str]]]] 10 | sourceRanges: Optional[Sequence[str]] 11 | sourceTags: Sequence[str] 12 | targetTags: Sequence[str] 13 | 14 | 15 | class GceNetworkOptions(ResourceOptions): 16 | accessKey: str 17 | firewall: Mapping[str, FirewallOptions] 18 | name: str 19 | project: str 20 | serviceAccount: str 21 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_route.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | from nixops.resources import ResourceOptions 3 | from typing import Union 4 | from typing import Optional 5 | 6 | 7 | class GceRouteOptions(ResourceOptions): 8 | accessKey: str 9 | description: Optional[str] 10 | destination: Optional[str] 11 | name: str 12 | network: str 13 | nextHop: Optional[str] 14 | priority: int 15 | project: str 16 | serviceAccount: str 17 | tags: Optional[Sequence[str]] 18 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_static_ip.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from typing import Union 3 | from nixops.resources import ResourceOptions 4 | 5 | 6 | class GceStaticIpOptions(ResourceOptions): 7 | accessKey: str 8 | ipAddress: Optional[str] 9 | name: str 10 | project: str 11 | publicIPv4: Optional[str] 12 | region: str 13 | serviceAccount: str 14 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gce_target_pool.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from nixops.resources import ResourceOptions 3 | from typing import Optional 4 | from typing import Sequence 5 | 6 | 7 | class GceTargetPoolOptions(ResourceOptions): 8 | accessKey: str 9 | healthCheck: Optional[str] 10 | machines: Sequence[str] 11 | name: str 12 | project: str 13 | region: str 14 | serviceAccount: str 15 | -------------------------------------------------------------------------------- /nixops_gcp/resources/types/gse_bucket.py: -------------------------------------------------------------------------------- 1 | from nixops.resources import ResourceOptions 2 | from typing import Union 3 | from typing import Sequence 4 | from typing import Optional 5 | 6 | 7 | class WebsiteOptions(ResourceOptions): 8 | mainPageSuffix: Optional[str] 9 | notFoundPage: Optional[str] 10 | 11 | 12 | class VersioningOptions(ResourceOptions): 13 | enabled: bool 14 | 15 | 16 | class LoggingOptions(ResourceOptions): 17 | logBucket: Optional[str] 18 | logObjectPrefix: Optional[str] 19 | 20 | 21 | class ConditionsOptions(ResourceOptions): 22 | age: Optional[int] 23 | createdBefore: Optional[str] 24 | isLive: Optional[bool] 25 | numberOfNewerVersions: Optional[int] 26 | 27 | 28 | class LifecycleOptions(ResourceOptions): 29 | action: str 30 | conditions: ConditionsOptions 31 | 32 | 33 | class CorsOptions(ResourceOptions): 34 | maxAgeSeconds: Optional[int] 35 | methods: Sequence[str] 36 | origins: Sequence[str] 37 | responseHeaders: Sequence[str] 38 | 39 | 40 | class GseBucketOptions(ResourceOptions): 41 | accessKey: str 42 | cors: Sequence[CorsOptions] 43 | lifecycle: Sequence[LifecycleOptions] 44 | location: str 45 | logging: LoggingOptions 46 | name: str 47 | project: str 48 | serviceAccount: str 49 | storageClass: str 50 | versioning: VersioningOptions 51 | website: WebsiteOptions 52 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "apache-libcloud" 5 | version = "3.7.0" 6 | description = "A standard Python library that abstracts away differences among multiple cloud provider APIs. For more information and documentation, please see https://libcloud.apache.org" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.6, <4" 10 | files = [ 11 | {file = "apache-libcloud-3.7.0.tar.gz", hash = "sha256:148a9e50069654432a7d34997954e91434dd38ebf68832eb9c75d442b3e62fad"}, 12 | {file = "apache_libcloud-3.7.0-py2.py3-none-any.whl", hash = "sha256:027a9aff2c01db9c8e6f9f94b6eb44b3153d82702c42bfbe7af5624dabf1f950"}, 13 | ] 14 | 15 | [package.dependencies] 16 | requests = ">=2.26.0" 17 | 18 | [[package]] 19 | name = "black" 20 | version = "22.12.0" 21 | description = "The uncompromising code formatter." 22 | category = "dev" 23 | optional = false 24 | python-versions = ">=3.7" 25 | files = [ 26 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 27 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 28 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 29 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 30 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 31 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 32 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 33 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 34 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 35 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 36 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 37 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 38 | ] 39 | 40 | [package.dependencies] 41 | click = ">=8.0.0" 42 | mypy-extensions = ">=0.4.3" 43 | pathspec = ">=0.9.0" 44 | platformdirs = ">=2" 45 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 46 | 47 | [package.extras] 48 | colorama = ["colorama (>=0.4.3)"] 49 | d = ["aiohttp (>=3.7.4)"] 50 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 51 | uvloop = ["uvloop (>=0.15.2)"] 52 | 53 | [[package]] 54 | name = "certifi" 55 | version = "2022.12.7" 56 | description = "Python package for providing Mozilla's CA Bundle." 57 | category = "main" 58 | optional = false 59 | python-versions = ">=3.6" 60 | files = [ 61 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 62 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 63 | ] 64 | 65 | [[package]] 66 | name = "cffi" 67 | version = "1.15.1" 68 | description = "Foreign Function Interface for Python calling C code." 69 | category = "main" 70 | optional = false 71 | python-versions = "*" 72 | files = [ 73 | {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, 74 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, 75 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, 76 | {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, 77 | {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, 78 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, 79 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, 80 | {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, 81 | {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, 82 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, 83 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, 84 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, 85 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, 86 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, 87 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, 88 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, 89 | {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, 90 | {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, 91 | {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, 92 | {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, 93 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, 94 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, 95 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, 96 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, 97 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, 98 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, 99 | {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, 100 | {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, 101 | {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, 102 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, 103 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, 104 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, 105 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, 106 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, 107 | {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, 108 | {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, 109 | {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, 110 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, 111 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, 112 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, 113 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, 114 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, 115 | {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, 116 | {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, 117 | {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, 118 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, 119 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, 120 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, 121 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, 122 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, 123 | {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, 124 | {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, 125 | {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, 126 | {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, 127 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, 128 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, 129 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, 130 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, 131 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, 132 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, 133 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, 134 | {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, 135 | {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, 136 | {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, 137 | ] 138 | 139 | [package.dependencies] 140 | pycparser = "*" 141 | 142 | [[package]] 143 | name = "charset-normalizer" 144 | version = "3.1.0" 145 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 146 | category = "main" 147 | optional = false 148 | python-versions = ">=3.7.0" 149 | files = [ 150 | {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, 151 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, 152 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, 153 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, 154 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, 155 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, 156 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, 157 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, 158 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, 159 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, 160 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, 161 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, 162 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, 163 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, 164 | {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, 165 | {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, 166 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, 167 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, 168 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, 169 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, 170 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, 171 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, 172 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, 173 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, 174 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, 175 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, 176 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, 177 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, 178 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, 179 | {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, 180 | {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, 181 | {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, 182 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, 183 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, 184 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, 185 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, 186 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, 187 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, 188 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, 189 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, 190 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, 191 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, 192 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, 193 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, 194 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, 195 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, 196 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, 197 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, 198 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, 199 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, 200 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, 201 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, 202 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, 203 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, 204 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, 205 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, 206 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, 207 | {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, 208 | {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, 209 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, 210 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, 211 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, 212 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, 213 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, 214 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, 215 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, 216 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, 217 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, 218 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, 219 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, 220 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, 221 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, 222 | {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, 223 | {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, 224 | {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, 225 | ] 226 | 227 | [[package]] 228 | name = "click" 229 | version = "8.1.3" 230 | description = "Composable command line interface toolkit" 231 | category = "dev" 232 | optional = false 233 | python-versions = ">=3.7" 234 | files = [ 235 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 236 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 237 | ] 238 | 239 | [package.dependencies] 240 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 241 | 242 | [[package]] 243 | name = "colorama" 244 | version = "0.4.6" 245 | description = "Cross-platform colored terminal text." 246 | category = "dev" 247 | optional = false 248 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 249 | files = [ 250 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 251 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 252 | ] 253 | 254 | [[package]] 255 | name = "cryptography" 256 | version = "40.0.1" 257 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 258 | category = "main" 259 | optional = false 260 | python-versions = ">=3.6" 261 | files = [ 262 | {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:918cb89086c7d98b1b86b9fdb70c712e5a9325ba6f7d7cfb509e784e0cfc6917"}, 263 | {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9618a87212cb5200500e304e43691111570e1f10ec3f35569fdfcd17e28fd797"}, 264 | {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4805a4ca729d65570a1b7cac84eac1e431085d40387b7d3bbaa47e39890b88"}, 265 | {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63dac2d25c47f12a7b8aa60e528bfb3c51c5a6c5a9f7c86987909c6c79765554"}, 266 | {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0a4e3406cfed6b1f6d6e87ed243363652b2586b2d917b0609ca4f97072994405"}, 267 | {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1e0af458515d5e4028aad75f3bb3fe7a31e46ad920648cd59b64d3da842e4356"}, 268 | {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d8aa3609d337ad85e4eb9bb0f8bcf6e4409bfb86e706efa9a027912169e89122"}, 269 | {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cf91e428c51ef692b82ce786583e214f58392399cf65c341bc7301d096fa3ba2"}, 270 | {file = "cryptography-40.0.1-cp36-abi3-win32.whl", hash = "sha256:650883cc064297ef3676b1db1b7b1df6081794c4ada96fa457253c4cc40f97db"}, 271 | {file = "cryptography-40.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:a805a7bce4a77d51696410005b3e85ae2839bad9aa38894afc0aa99d8e0c3160"}, 272 | {file = "cryptography-40.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd033d74067d8928ef00a6b1327c8ea0452523967ca4463666eeba65ca350d4c"}, 273 | {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d36bbeb99704aabefdca5aee4eba04455d7a27ceabd16f3b3ba9bdcc31da86c4"}, 274 | {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:32057d3d0ab7d4453778367ca43e99ddb711770477c4f072a51b3ca69602780a"}, 275 | {file = "cryptography-40.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f5d7b79fa56bc29580faafc2ff736ce05ba31feaa9d4735048b0de7d9ceb2b94"}, 276 | {file = "cryptography-40.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7c872413353c70e0263a9368c4993710070e70ab3e5318d85510cc91cce77e7c"}, 277 | {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:28d63d75bf7ae4045b10de5413fb1d6338616e79015999ad9cf6fc538f772d41"}, 278 | {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6f2bbd72f717ce33100e6467572abaedc61f1acb87b8d546001328d7f466b778"}, 279 | {file = "cryptography-40.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc3a621076d824d75ab1e1e530e66e7e8564e357dd723f2533225d40fe35c60c"}, 280 | {file = "cryptography-40.0.1.tar.gz", hash = "sha256:2803f2f8b1e95f614419926c7e6f55d828afc614ca5ed61543877ae668cc3472"}, 281 | ] 282 | 283 | [package.dependencies] 284 | cffi = ">=1.12" 285 | 286 | [package.extras] 287 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 288 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 289 | pep8test = ["black", "check-manifest", "mypy", "ruff"] 290 | sdist = ["setuptools-rust (>=0.11.4)"] 291 | ssh = ["bcrypt (>=3.1.5)"] 292 | test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] 293 | test-randomorder = ["pytest-randomly"] 294 | tox = ["tox"] 295 | 296 | [[package]] 297 | name = "idna" 298 | version = "3.4" 299 | description = "Internationalized Domain Names in Applications (IDNA)" 300 | category = "main" 301 | optional = false 302 | python-versions = ">=3.5" 303 | files = [ 304 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 305 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 306 | ] 307 | 308 | [[package]] 309 | name = "mypy" 310 | version = "0.961" 311 | description = "Optional static typing for Python" 312 | category = "dev" 313 | optional = false 314 | python-versions = ">=3.6" 315 | files = [ 316 | {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"}, 317 | {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"}, 318 | {file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"}, 319 | {file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"}, 320 | {file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"}, 321 | {file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"}, 322 | {file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"}, 323 | {file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"}, 324 | {file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"}, 325 | {file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"}, 326 | {file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"}, 327 | {file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"}, 328 | {file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"}, 329 | {file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"}, 330 | {file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"}, 331 | {file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"}, 332 | {file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"}, 333 | {file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"}, 334 | {file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"}, 335 | {file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"}, 336 | {file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"}, 337 | {file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"}, 338 | {file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"}, 339 | ] 340 | 341 | [package.dependencies] 342 | mypy-extensions = ">=0.4.3" 343 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 344 | typing-extensions = ">=3.10" 345 | 346 | [package.extras] 347 | dmypy = ["psutil (>=4.0)"] 348 | python2 = ["typed-ast (>=1.4.0,<2)"] 349 | reports = ["lxml"] 350 | 351 | [[package]] 352 | name = "mypy-extensions" 353 | version = "1.0.0" 354 | description = "Type system extensions for programs checked with the mypy type checker." 355 | category = "dev" 356 | optional = false 357 | python-versions = ">=3.5" 358 | files = [ 359 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 360 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 361 | ] 362 | 363 | [[package]] 364 | name = "nixops" 365 | version = "2.0.0" 366 | description = "NixOS cloud provisioning and deployment tool" 367 | category = "main" 368 | optional = false 369 | python-versions = "^3.10" 370 | files = [] 371 | develop = false 372 | 373 | [package.dependencies] 374 | pluggy = "^1.0.0" 375 | PrettyTable = "^0.7.2" 376 | typeguard = "^2.7.1" 377 | typing-extensions = "^3.7.4" 378 | 379 | [package.source] 380 | type = "git" 381 | url = "https://github.com/NixOS/nixops.git" 382 | reference = "master" 383 | resolved_reference = "fc9b55c55da62f949028143b974f67fdc7f40c8b" 384 | 385 | [[package]] 386 | name = "nixos-modules-contrib" 387 | version = "0.1.0" 388 | description = "NixOS modules that don't quite belong in NixOS." 389 | category = "main" 390 | optional = false 391 | python-versions = "^3.7" 392 | files = [] 393 | develop = false 394 | 395 | [package.dependencies] 396 | nixops = {git = "https://github.com/NixOS/nixops.git", rev = "master"} 397 | 398 | [package.source] 399 | type = "git" 400 | url = "https://github.com/nix-community/nixos-modules-contrib.git" 401 | reference = "master" 402 | resolved_reference = "81a1c2ef424dcf596a97b2e46a58ca73a1dd1ff8" 403 | 404 | [[package]] 405 | name = "nose" 406 | version = "1.3.7" 407 | description = "nose extends unittest to make testing easier" 408 | category = "dev" 409 | optional = false 410 | python-versions = "*" 411 | files = [ 412 | {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"}, 413 | {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"}, 414 | {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"}, 415 | ] 416 | 417 | [[package]] 418 | name = "pathspec" 419 | version = "0.11.1" 420 | description = "Utility library for gitignore style pattern matching of file paths." 421 | category = "dev" 422 | optional = false 423 | python-versions = ">=3.7" 424 | files = [ 425 | {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, 426 | {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, 427 | ] 428 | 429 | [[package]] 430 | name = "platformdirs" 431 | version = "3.2.0" 432 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 433 | category = "dev" 434 | optional = false 435 | python-versions = ">=3.7" 436 | files = [ 437 | {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, 438 | {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, 439 | ] 440 | 441 | [package.extras] 442 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] 443 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 444 | 445 | [[package]] 446 | name = "pluggy" 447 | version = "1.0.0" 448 | description = "plugin and hook calling mechanisms for python" 449 | category = "main" 450 | optional = false 451 | python-versions = ">=3.6" 452 | files = [ 453 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 454 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 455 | ] 456 | 457 | [package.extras] 458 | dev = ["pre-commit", "tox"] 459 | testing = ["pytest", "pytest-benchmark"] 460 | 461 | [[package]] 462 | name = "prettytable" 463 | version = "0.7.2" 464 | description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format." 465 | category = "main" 466 | optional = false 467 | python-versions = "*" 468 | files = [ 469 | {file = "prettytable-0.7.2.tar.bz2", hash = "sha256:853c116513625c738dc3ce1aee148b5b5757a86727e67eff6502c7ca59d43c36"}, 470 | {file = "prettytable-0.7.2.tar.gz", hash = "sha256:2d5460dc9db74a32bcc8f9f67de68b2c4f4d2f01fa3bd518764c69156d9cacd9"}, 471 | {file = "prettytable-0.7.2.zip", hash = "sha256:a53da3b43d7a5c229b5e3ca2892ef982c46b7923b51e98f0db49956531211c4f"}, 472 | ] 473 | 474 | [[package]] 475 | name = "pycparser" 476 | version = "2.21" 477 | description = "C parser in Python" 478 | category = "main" 479 | optional = false 480 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 481 | files = [ 482 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 483 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 484 | ] 485 | 486 | [[package]] 487 | name = "requests" 488 | version = "2.28.2" 489 | description = "Python HTTP for Humans." 490 | category = "main" 491 | optional = false 492 | python-versions = ">=3.7, <4" 493 | files = [ 494 | {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, 495 | {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, 496 | ] 497 | 498 | [package.dependencies] 499 | certifi = ">=2017.4.17" 500 | charset-normalizer = ">=2,<4" 501 | idna = ">=2.5,<4" 502 | urllib3 = ">=1.21.1,<1.27" 503 | 504 | [package.extras] 505 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 506 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 507 | 508 | [[package]] 509 | name = "tomli" 510 | version = "2.0.1" 511 | description = "A lil' TOML parser" 512 | category = "dev" 513 | optional = false 514 | python-versions = ">=3.7" 515 | files = [ 516 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 517 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 518 | ] 519 | 520 | [[package]] 521 | name = "typeguard" 522 | version = "2.13.3" 523 | description = "Run-time type checker for Python" 524 | category = "main" 525 | optional = false 526 | python-versions = ">=3.5.3" 527 | files = [ 528 | {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, 529 | {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, 530 | ] 531 | 532 | [package.extras] 533 | doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 534 | test = ["mypy", "pytest", "typing-extensions"] 535 | 536 | [[package]] 537 | name = "typing-extensions" 538 | version = "3.10.0.2" 539 | description = "Backported and Experimental Type Hints for Python 3.5+" 540 | category = "main" 541 | optional = false 542 | python-versions = "*" 543 | files = [ 544 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 545 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 546 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 547 | ] 548 | 549 | [[package]] 550 | name = "urllib3" 551 | version = "1.26.15" 552 | description = "HTTP library with thread-safe connection pooling, file post, and more." 553 | category = "main" 554 | optional = false 555 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 556 | files = [ 557 | {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, 558 | {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, 559 | ] 560 | 561 | [package.extras] 562 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 563 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 564 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 565 | 566 | [metadata] 567 | lock-version = "2.0" 568 | python-versions = "^3.10" 569 | content-hash = "9ef8be038a24d115b7b1fa429835f9107c592b868a1d7e96ee9b519068b1ba75" 570 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nixops_gcp" 3 | version = "1.0" 4 | description = "NixOps backend for Google Cloud Platform" 5 | authors = ["Evgeny Egorochkin "] 6 | maintainers = ["Amine Chikhaoui "] 7 | license = "MIT" 8 | include = [ "nixops_gcp/nix/*.nix" ] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.10" 12 | apache-libcloud = "^3.7.0" 13 | # implicit transitive dependency of apache-libcloud 14 | cryptography = "40.0.1" 15 | nixops = {git = "https://github.com/NixOS/nixops.git", rev = "master"} 16 | nixos-modules-contrib = {git = "https://github.com/nix-community/nixos-modules-contrib.git", rev = "master"} 17 | 18 | [tool.poetry.dev-dependencies] 19 | nose = "^1.3.7" 20 | mypy = "^0.961" 21 | black = "^22.6.0" 22 | 23 | [tool.poetry.plugins."nixops"] 24 | gcp = "nixops_gcp.plugin" 25 | 26 | [build-system] 27 | requires = ["poetry>=0.12"] 28 | build-backend = "poetry.masonry.api" 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy-libcloud.*] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /tests/functional/single_machine_gce_base.nix: -------------------------------------------------------------------------------- 1 | let 2 | region = "europe-west1-b"; 3 | in 4 | { 5 | machine = 6 | { resources, ... }: 7 | { 8 | deployment.targetEnv = "gce"; 9 | deployment.gce = { 10 | inherit region; 11 | instanceType = "g1-small"; 12 | rootDiskSize = 5; 13 | tags = [ "test" "instance" ]; 14 | metadata.random = "mess"; 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /tests/functional/single_machine_static_ip_gce.nix: -------------------------------------------------------------------------------- 1 | let 2 | region = "europe-west1-b"; 3 | in 4 | { 5 | machine = 6 | { resources, ... }: 7 | { 8 | deployment.targetEnv = "gce"; 9 | deployment.gce = { 10 | inherit region; 11 | instanceType = "g1-small"; 12 | rootDiskSize = 5; 13 | tags = [ "test" "instance" ]; 14 | metadata.random = "mess"; 15 | }; 16 | }; 17 | 18 | ip = 19 | { resources, ... }: 20 | { 21 | resources.gceStaticIPs.endpointIP = credentials // { 22 | region = region; 23 | name = "endpoint-ip"; 24 | }; 25 | }; 26 | } 27 | --------------------------------------------------------------------------------