Class: Nvoi::External::Cloud::Scaleway
- Defined in:
- lib/nvoi/external/cloud/scaleway.rb
Overview
Scaleway provider implements the compute provider interface for Scaleway Cloud
Constant Summary collapse
- INSTANCE_API_BASE =
"https://api.scaleway.com/instance/v1"- VPC_API_BASE =
"https://api.scaleway.com/vpc/v2"- BLOCK_API_BASE =
"https://api.scaleway.com/block/v1alpha1"- VALID_ZONES =
%w[ fr-par-1 fr-par-2 fr-par-3 nl-ams-1 nl-ams-2 nl-ams-3 pl-waw-1 pl-waw-2 pl-waw-3 ].freeze
Instance Attribute Summary collapse
-
#project_id ⇒ Object
readonly
Returns the value of attribute project_id.
-
#region ⇒ Object
readonly
Returns the value of attribute region.
-
#zone ⇒ Object
readonly
Returns the value of attribute zone.
Instance Method Summary collapse
- #attach_volume(volume_id, server_id) ⇒ Object
- #create_server(opts) ⇒ Object
-
#create_volume(opts) ⇒ Object
Volume operations.
- #delete_firewall(id) ⇒ Object
- #delete_network(id) ⇒ Object
- #delete_server(id) ⇒ Object
- #delete_volume(id) ⇒ Object
- #detach_volume(volume_id) ⇒ Object
-
#find_or_create_firewall(name) ⇒ Object
Firewall operations (Security Groups).
-
#find_or_create_network(name) ⇒ Object
Network operations.
-
#find_server(name) ⇒ Object
Server operations.
- #find_server_by_id(id) ⇒ Object
- #get_firewall_by_name(name) ⇒ Object
- #get_network_by_name(name) ⇒ Object
- #get_volume(id) ⇒ Object
- #get_volume_by_name(name) ⇒ Object
-
#initialize(secret_key, project_id, zone: "fr-par-1") ⇒ Scaleway
constructor
A new instance of Scaleway.
-
#list_server_types ⇒ Object
List available server types for onboarding.
- #list_servers ⇒ Object
-
#list_zones ⇒ Object
List available zones for onboarding.
-
#server_ip(server_name) ⇒ Object
Server IP lookup for exec/db commands.
- #validate_credentials ⇒ Object
-
#validate_instance_type(instance_type) ⇒ Object
Validation operations.
- #validate_region(region) ⇒ Object
- #wait_for_device_path(volume_id, ssh) ⇒ Object
- #wait_for_server(server_id, max_attempts) ⇒ Object
Constructor Details
#initialize(secret_key, project_id, zone: "fr-par-1") ⇒ Scaleway
Returns a new instance of Scaleway.
21 22 23 24 25 26 27 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 21 def initialize(secret_key, project_id, zone: "fr-par-1") @secret_key = secret_key @project_id = project_id @zone = zone @region = zone_to_region(zone) @conn = build_connection end |
Instance Attribute Details
#project_id ⇒ Object (readonly)
Returns the value of attribute project_id.
29 30 31 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 29 def project_id @project_id end |
#region ⇒ Object (readonly)
Returns the value of attribute region.
29 30 31 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 29 def region @region end |
#zone ⇒ Object (readonly)
Returns the value of attribute zone.
29 30 31 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 29 def zone @zone end |
Instance Method Details
#attach_volume(volume_id, server_id) ⇒ Object
241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 241 def attach_volume(volume_id, server_id) server = get_server_api(server_id) raise Errors::VolumeError, "server not found: #{server_id}" unless server wait_for_volume_available(volume_id) current_volumes = server["volumes"] || {} next_index = current_volumes.keys.map(&:to_i).max.to_i + 1 new_volumes = current_volumes.dup new_volumes[next_index.to_s] = { id: volume_id, volume_type: "sbs_volume" } patch(instance_url("/servers/#{server_id}"), { volumes: new_volumes }) end |
#create_server(opts) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 128 def create_server(opts) # Validate server type server_types = list_server_types_api unless server_types.key?(opts.type) raise Errors::ValidationError, "invalid server type: #{opts.type}" end # Resolve image image = find_image(opts.image) raise Errors::ValidationError, "invalid image: #{opts.image}" unless image create_opts = { name: opts.name, commercial_type: opts.type, image: image["id"], project: @project_id, boot_type: "local", tags: [] } # Add security group if provided unless opts.firewall_id.blank? create_opts[:security_group] = opts.firewall_id end server = post(instance_url("/servers"), create_opts)["server"] # Set cloud-init user data if provided unless opts.user_data.blank? set_user_data(server["id"], "cloud-init", opts.user_data) end # Power on the server server_action(server["id"], "poweron") # Attach to private network if provided unless opts.network_id.blank? wait_for_server_state(server["id"], "running", 30) create_private_nic(server["id"], opts.network_id) end to_server(get_server_api(server["id"])) end |
#create_volume(opts) ⇒ Object
Volume operations
207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 207 def create_volume(opts) server = get_server_api(opts.server_id) raise Errors::VolumeError, "server not found: #{opts.server_id}" unless server volume = post(block_url("/volumes"), { name: opts.name, perf_iops: 5000, from_empty: { size: opts.size * 1_000_000_000 }, project_id: @project_id }) to_volume(volume) end |
#delete_firewall(id) ⇒ Object
102 103 104 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 102 def delete_firewall(id) delete(instance_url("/security_groups/#{id}")) end |
#delete_network(id) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 52 def delete_network(id) # First detach all servers from this network list_servers_api.each do |server| nics = list_private_nics(server["id"]) nics.each do |nic| next unless nic["private_network_id"] == id delete_private_nic(server["id"], nic["id"]) rescue StandardError # Ignore cleanup errors end end delete(vpc_url("/private-networks/#{id}")) end |
#delete_server(id) ⇒ Object
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 183 def delete_server(id) # Delete private NICs first nics = list_private_nics(id) nics.each do |nic| delete_private_nic(id, nic["id"]) rescue StandardError # Ignore cleanup errors end # Terminate server (this also stops and deletes) server_action(id, "terminate") rescue StandardError => e # If terminate fails, try poweroff then delete begin server_action(id, "poweroff") sleep(5) delete(instance_url("/servers/#{id}")) rescue StandardError raise e end end |
#delete_volume(id) ⇒ Object
237 238 239 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 237 def delete_volume(id) delete(block_url("/volumes/#{id}")) end |
#detach_volume(volume_id) ⇒ Object
256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 256 def detach_volume(volume_id) list_servers_api.each do |server| volumes = server["volumes"] || {} volumes.each do |idx, vol| next unless vol["id"] == volume_id new_volumes = volumes.reject { |k, _| k == idx } patch(instance_url("/servers/#{server["id"]}"), { volumes: new_volumes }) return end end end |
#find_or_create_firewall(name) ⇒ Object
Firewall operations (Security Groups)
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 70 def find_or_create_firewall(name) sg = find_security_group_by_name(name) return to_firewall(sg) if sg sg = post(instance_url("/security_groups"), { name:, project: @project_id, stateful: true, inbound_default_policy: "drop", outbound_default_policy: "accept" })["security_group"] # Add SSH rule post(instance_url("/security_groups/#{sg["id"]}/rules"), { protocol: "TCP", direction: "inbound", action: "accept", ip_range: "0.0.0.0/0", dest_port_from: 22, dest_port_to: 22 }) to_firewall(sg) end |
#find_or_create_network(name) ⇒ Object
Network operations
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 33 def find_or_create_network(name) network = find_network_by_name(name) return to_network(network) if network network = post(vpc_url("/private-networks"), { name:, project_id: @project_id }) to_network(network) end |
#find_server(name) ⇒ Object
Server operations
108 109 110 111 112 113 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 108 def find_server(name) server = find_server_by_name(name) return nil unless server to_server(server, fetch_private_ip: true) end |
#find_server_by_id(id) ⇒ Object
115 116 117 118 119 120 121 122 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 115 def find_server_by_id(id) server = get(instance_url("/servers/#{id}"))["server"] return nil unless server to_server(server, fetch_private_ip: true) rescue Errors::NotFoundError nil end |
#get_firewall_by_name(name) ⇒ Object
95 96 97 98 99 100 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 95 def get_firewall_by_name(name) sg = find_security_group_by_name(name) raise Errors::FirewallError, "security group not found: #{name}" unless sg to_firewall(sg) end |
#get_network_by_name(name) ⇒ Object
45 46 47 48 49 50 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 45 def get_network_by_name(name) network = find_network_by_name(name) raise Errors::NetworkError, "network not found: #{name}" unless network to_network(network) end |
#get_volume(id) ⇒ Object
221 222 223 224 225 226 227 228 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 221 def get_volume(id) volume = get(block_url("/volumes/#{id}")) return nil unless volume to_volume(volume) rescue Errors::NotFoundError nil end |
#get_volume_by_name(name) ⇒ Object
230 231 232 233 234 235 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 230 def get_volume_by_name(name) volume = list_volumes.find { |v| v["name"] == name } return nil unless volume to_volume(volume) end |
#list_server_types ⇒ Object
List available server types for onboarding
314 315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 314 def list_server_types list_server_types_api.map do |name, info| arch = info.dig("arch") || "x86_64" { name:, cores: info.dig("ncpus"), ram: info.dig("ram"), hourly_price: info.dig("hourly_price"), architecture: arch.include?("arm") ? "arm64" : "x86" } end end |
#list_servers ⇒ Object
124 125 126 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 124 def list_servers list_servers_api.map { |s| to_server(s) } end |
#list_zones ⇒ Object
List available zones for onboarding
328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 328 def list_zones VALID_ZONES.map do |z| parts = z.split("-") city = case parts[0..1].join("-") when "fr-par" then "Paris" when "nl-ams" then "Amsterdam" when "pl-waw" then "Warsaw" else parts[0..1].join("-") end { name: z, city: } end end |
#server_ip(server_name) ⇒ Object
Server IP lookup for exec/db commands
308 309 310 311 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 308 def server_ip(server_name) server = find_server(server_name) server&.public_ipv4 end |
#validate_credentials ⇒ Object
300 301 302 303 304 305 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 300 def validate_credentials list_server_types_api true rescue Errors::AuthenticationError => e raise Errors::ValidationError, "scaleway credentials invalid: #{e.message}" end |
#validate_instance_type(instance_type) ⇒ Object
Validation operations
283 284 285 286 287 288 289 290 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 283 def validate_instance_type(instance_type) server_types = list_server_types_api unless server_types.key?(instance_type) raise Errors::ValidationError, "invalid scaleway server type: #{instance_type}" end true end |
#validate_region(region) ⇒ Object
292 293 294 295 296 297 298 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 292 def validate_region(region) unless VALID_ZONES.include?(region) raise Errors::ValidationError, "invalid scaleway zone: #{region}. Valid: #{VALID_ZONES.join(", ")}" end true end |
#wait_for_device_path(volume_id, ssh) ⇒ Object
269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 269 def wait_for_device_path(volume_id, ssh) # Scaleway doesn't provide device_path in API # Find device by volume ID in /dev/disk/by-id/ Utils::Retry.poll(max_attempts: 30, interval: 2) do output = ssh.execute("ls /dev/disk/by-id/ 2>/dev/null | grep -i '#{volume_id}' || true").strip next nil if output.empty? device_name = output.lines.first.strip "/dev/disk/by-id/#{device_name}" end end |
#wait_for_server(server_id, max_attempts) ⇒ Object
172 173 174 175 176 177 178 179 180 181 |
# File 'lib/nvoi/external/cloud/scaleway.rb', line 172 def wait_for_server(server_id, max_attempts) server = Utils::Retry.poll(max_attempts:, interval: Utils::Constants::SERVER_READY_INTERVAL) do s = get_server_api(server_id) to_server(s) if s["state"] == "running" && s.dig("public_ip", "address") end raise Errors::ServerCreationError, "server did not become running after #{max_attempts} attempts" unless server server end |